diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 1664 |
1 files changed, 63 insertions, 1601 deletions
@@ -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<CommandOutcome> { 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<CommandOutcome> { Ok(CommandOutcome::Handled(exit_code)) } -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 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) - } - } -} - fn run_config_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_config_help(); @@ -204,206 +157,6 @@ fn key_config_backup_path(path: &std::path::Path) -> std::path::PathBuf { path.with_file_name(format!("{file_name}.bak-keybind-v2-{timestamp}")) } -#[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(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(); - 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<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 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 run_terminal_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_terminal_help(); @@ -423,34 +176,6 @@ fn run_terminal_command(args: &[String]) -> std::io::Result<i32> { } } -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 run_wait_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_wait_help(); @@ -471,27 +196,6 @@ fn run_wait_command(args: &[String]) -> std::io::Result<i32> { } } -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 run_session_command(args: &[String]) -> std::io::Result<i32> { let Some(subcommand) = args.first().map(|arg| arg.as_str()) else { print_session_help(); @@ -514,27 +218,6 @@ fn run_session_command(args: &[String]) -> std::io::Result<i32> { } } -fn server_stop(args: &[String]) -> std::io::Result<i32> { - 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<i32> { - 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<i32> { if matches!( args.first().map(String::as_str), @@ -623,541 +306,6 @@ fn session_delete(args: &[String]) -> std::io::Result<i32> { } } -fn workspace_list(args: &[String]) -> std::io::Result<i32> { - 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<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); - } - } - } - - 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<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); - } - - 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<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); - } - - 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<i32> { - if args.len() < 2 { - eprintln!("usage: herdr workspace rename <workspace_id> <label>"); - return Ok(2); - } - - print_response(&send_request(&Request { - id: "cli:workspace:rename".into(), - method: Method::WorkspaceRename(WorkspaceRenameParams { - workspace_id: 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); - } - - print_response(&send_request(&Request { - id: "cli:workspace:close".into(), - method: Method::WorkspaceClose(WorkspaceTarget { - workspace_id: normalize_workspace_id(raw_workspace_id), - }), - })?) -} - -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(normalize_workspace_id(value)); - index += 2; - } - other => { - eprintln!("unknown option: {other}"); - return Ok(2); - } - } - } - - print_response(&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(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); - } - } - } - - print_response(&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); - } - - print_response(&send_request(&Request { - id: "cli:tab:get".into(), - method: Method::TabGet(TabTarget { - tab_id: 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); - } - - print_response(&send_request(&Request { - id: "cli:tab:focus".into(), - method: Method::TabFocus(TabTarget { - tab_id: 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); - } - - print_response(&send_request(&Request { - id: "cli:tab:rename".into(), - method: Method::TabRename(TabRenameParams { - tab_id: 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); - } - - print_response(&send_request(&Request { - id: "cli:tab:close".into(), - method: Method::TabClose(TabTarget { - tab_id: normalize_tab_id(raw_tab_id), - }), - })?) -} - -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(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(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(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); - } - } - } - - print_response(&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); - } - - print_response(&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); - } - - print_response(&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); - } - - print_response(&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 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(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), - }] - }; - - 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> { - send_request(&Request { - id: request_id.into(), - method: Method::AgentGet(AgentTarget { - target: target.to_owned(), - }), - }) -} - fn terminal_attach(args: &[String]) -> std::io::Result<i32> { let (terminal_id, takeover) = match parse_attach_target( args, @@ -1170,7 +318,7 @@ fn terminal_attach(args: &[String]) -> std::io::Result<i32> { Ok(0) } -fn parse_attach_target(args: &[String], usage: &str) -> Result<(String, bool), i32> { +pub(super) fn parse_attach_target(args: &[String], usage: &str) -> Result<(String, bool), i32> { let Some(target) = args.first() else { eprintln!("{usage}"); return Err(2); @@ -1192,574 +340,6 @@ fn parse_attach_target(args: &[String], usage: &str) -> Result<(String, bool), i Ok((target.clone(), takeover)) } -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(" ")) - }; - - print_response(&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); - } - - print_response(&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 = 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(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 = 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); - } - } - } - - print_response(&send_request(&Request { - id: "cli:agent:read".into(), - method: Method::AgentRead(AgentReadParams { - target: target.clone(), - source, - lines, - format, - strip_ansi, - }), - })?) -} - -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(normalize_workspace_id(value)); - index += 2; - } - other => { - eprintln!("unknown option: {other}"); - return Ok(2); - } - } - } - - print_response(&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); - } - - print_response(&send_request(&Request { - id: "cli:pane:get".into(), - method: Method::PaneGet(PaneTarget { - pane_id: 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(" ")) - }; - - print_response(&send_request(&Request { - id: "cli:pane:rename".into(), - method: Method::PaneRename(PaneRenameParams { - pane_id: 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 = 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 = 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(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 = 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 = 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 = 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(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); - }; - - print_response(&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); - } - - print_response(&send_request(&Request { - id: "cli:pane:close".into(), - method: Method::PaneClose(PaneTarget { - pane_id: 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 = normalize_pane_id(&args[0]); - let text = args[1..].join(" "); - 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 = normalize_pane_id(&args[0]); - let keys = args[1..].to_vec(); - 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 = normalize_pane_id(&args[0]); - let text = args[1..].join(" "); - 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 = 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(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(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); - }; - - send_ok_request(Method::PaneReportAgent(PaneReportAgentParams { - pane_id, - source, - agent, - state, - message, - custom_status, - seq, - })) -} - -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 wait_output(args: &[String]) -> std::io::Result<i32> { let Some(raw_pane_id) = args.first() else { eprintln!("usage: herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex]"); @@ -1912,58 +492,41 @@ fn wait_agent_status(args: &[String]) -> std::io::Result<i32> { ) } -fn wait_for_agent_change( +pub(super) fn wait_for_agent_change( request: Request, timeout_ms: Option<u64>, timeout_message: &str, ) -> std::io::Result<i32> { - let mut stream = UnixStream::connect(api::socket_path())?; - stream.write_all(serde_json::to_string(&request)?.as_bytes())?; - stream.write_all(b"\n")?; - stream.flush()?; - - if let Some(timeout_ms) = timeout_ms { - stream.set_read_timeout(Some(Duration::from_millis(timeout_ms)))?; - } - - let mut reader = BufReader::new(stream); - let mut ack = String::new(); - reader.read_line(&mut ack)?; - if ack.trim().is_empty() { - eprintln!("empty subscription ack"); - return Ok(1); - } - let ack_value: serde_json::Value = serde_json::from_str(&ack)?; - if ack_value.get("error").is_some() { - eprintln!("{}", serde_json::to_string(&ack_value).unwrap()); - return Ok(1); + let read_timeout = timeout_ms.map(Duration::from_millis); + let (ack, mut stream) = ApiClient::local() + .subscribe_value(&request, read_timeout) + .map_err(api_client_error_to_io)?; + if let Err(err) = crate::api::client::parse_response_value(ack) { + if let ApiClientError::ErrorResponse(response) = err { + eprintln!("{}", serde_json::to_string(&response).unwrap()); + return Ok(1); + } + return Err(api_client_error_to_io(err)); } - let mut event = String::new(); - match reader.read_line(&mut event) { - Ok(0) => { + match stream.next_event() { + Ok(None) => { eprintln!("subscription closed before event arrived"); Ok(1) } - Ok(_) => { - let event_value: serde_json::Value = serde_json::from_str(&event)?; + Ok(Some(event_value)) => { println!("{}", serde_json::to_string(&event_value).unwrap()); Ok(0) } - Err(err) - if matches!( - err.kind(), - std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock - ) => - { + Err(ApiClientError::Io(err)) if api_timeout_error(&err) => { eprintln!("{timeout_message}"); Ok(1) } - Err(err) => Err(err), + Err(err) => Err(api_client_error_to_io(err)), } } -fn print_response(response: &serde_json::Value) -> std::io::Result<i32> { +pub(super) fn print_response(response: &serde_json::Value) -> std::io::Result<i32> { if response.get("error").is_some() { eprintln!("{}", serde_json::to_string(response).unwrap()); return Ok(1); @@ -1973,7 +536,7 @@ fn print_response(response: &serde_json::Value) -> std::io::Result<i32> { Ok(0) } -fn send_ok_request(method: Method) -> std::io::Result<i32> { +pub(super) fn send_ok_request(method: Method) -> std::io::Result<i32> { let response = send_request(&Request { id: "cli:request".into(), method, @@ -1987,31 +550,39 @@ fn send_ok_request(method: Method) -> std::io::Result<i32> { Ok(0) } -fn send_request(request: &Request) -> std::io::Result<serde_json::Value> { - let mut stream = UnixStream::connect(api::socket_path())?; - stream.write_all(serde_json::to_string(request)?.as_bytes())?; - stream.write_all(b"\n")?; - stream.flush()?; +pub(super) fn send_request(request: &Request) -> std::io::Result<serde_json::Value> { + ApiClient::local() + .request_value(request) + .map_err(api_client_error_to_io) +} - let mut reader = BufReader::new(stream); - let mut line = String::new(); - reader.read_line(&mut line)?; - serde_json::from_str(&line).map_err(std::io::Error::other) +fn api_timeout_error(err: &std::io::Error) -> bool { + matches!( + err.kind(), + std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock + ) +} + +fn api_client_error_to_io(err: ApiClientError) -> std::io::Error { + match err { + ApiClientError::Io(err) => err, + err => std::io::Error::other(err), + } } -fn normalize_workspace_id(value: &str) -> String { +pub(super) fn normalize_workspace_id(value: &str) -> String { value.to_string() } -fn normalize_tab_id(value: &str) -> String { +pub(super) fn normalize_tab_id(value: &str) -> String { value.to_string() } -fn normalize_pane_id(value: &str) -> String { +pub(super) fn normalize_pane_id(value: &str) -> String { value.to_string() } -fn parse_split_direction(value: &str) -> std::io::Result<SplitDirection> { +pub(super) fn parse_split_direction(value: &str) -> std::io::Result<SplitDirection> { match value { "right" => Ok(SplitDirection::Right), "down" => Ok(SplitDirection::Down), @@ -2021,7 +592,7 @@ fn parse_split_direction(value: &str) -> std::io::Result<SplitDirection> { } } -fn parse_read_source(value: &str) -> std::io::Result<ReadSource> { +pub(super) fn parse_read_source(value: &str) -> std::io::Result<ReadSource> { match value { "visible" => Ok(ReadSource::Visible), "recent" => Ok(ReadSource::Recent), @@ -2032,7 +603,7 @@ fn parse_read_source(value: &str) -> std::io::Result<ReadSource> { } } -fn parse_read_format(value: &str) -> std::io::Result<ReadFormat> { +pub(super) fn parse_read_format(value: &str) -> std::io::Result<ReadFormat> { match value { "text" => Ok(ReadFormat::Text), "ansi" => Ok(ReadFormat::Ansi), @@ -2042,31 +613,6 @@ fn parse_read_format(value: &str) -> std::io::Result<ReadFormat> { } } -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 parse_agent_status(value: &str) -> std::io::Result<AgentStatus> { match value { "idle" => Ok(AgentStatus::Idle), @@ -2080,7 +626,7 @@ fn parse_agent_status(value: &str) -> std::io::Result<AgentStatus> { } } -fn parse_pane_agent_state(value: &str) -> std::io::Result<PaneAgentState> { +pub(super) fn parse_pane_agent_state(value: &str) -> std::io::Result<PaneAgentState> { match value { "idle" => Ok(PaneAgentState::Idle), "working" => Ok(PaneAgentState::Working), @@ -2092,13 +638,13 @@ fn parse_pane_agent_state(value: &str) -> std::io::Result<PaneAgentState> { } } -fn parse_u32_flag(flag: &str, value: &str) -> std::io::Result<u32> { +pub(super) fn parse_u32_flag(flag: &str, value: &str) -> std::io::Result<u32> { value .parse::<u32>() .map_err(|_| std::io::Error::other(format!("invalid value for {flag}: {value}"))) } -fn parse_u64_flag(flag: &str, value: &str) -> std::io::Result<u64> { +pub(super) fn parse_u64_flag(flag: &str, value: &str) -> std::io::Result<u64> { value .parse::<u64>() .map_err(|_| std::io::Error::other(format!("invalid value for {flag}: {value}"))) @@ -2166,86 +712,17 @@ fn print_session_error(code: &str, message: &str) { ); } -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"); -} - -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_config_help() { eprintln!("herdr config commands:"); eprintln!(" herdr config reset-keys back up config.toml and remove custom keybindings"); } -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>"); -} - -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>"); -} - -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" - ); -} - fn print_terminal_help() { eprintln!("herdr terminal commands:"); eprintln!(" herdr terminal attach <terminal_id> [--takeover]"); eprintln!(" detach from direct attach with ctrl+b q; send literal ctrl+b with ctrl+b ctrl+b"); } -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>"); -} - 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] [--raw]"); @@ -2254,21 +731,6 @@ fn print_wait_help() { ); } -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]"); -} - fn print_session_help() { eprintln!("herdr session commands:"); eprintln!(" herdr session list [--json]"); |
