aboutsummaryrefslogtreecommitdiff
path: root/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs1664
1 files changed, 63 insertions, 1601 deletions
diff --git a/src/cli.rs b/src/cli.rs
index e9fe6e81..fed93fba 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,20 +1,21 @@
-use std::io::{BufRead, BufReader, Write};
-use std::os::unix::net::UnixStream;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
-use crate::api;
+use crate::api::client::{ApiClient, ApiClientError};
use crate::api::schema::{
- AgentReadParams, AgentRenameParams, AgentSendParams, AgentStartParams, AgentStatus,
- AgentTarget, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneAgentState,
- PaneListParams, PaneReadParams, PaneRenameParams, PaneReportAgentParams, PaneSendInputParams,
- PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget, PaneWaitForOutputParams,
- PingParams, ReadFormat, ReadSource, Request, SplitDirection, Subscription, TabCreateParams,
- TabListParams, TabRenameParams, TabTarget, WorkspaceCreateParams, WorkspaceRenameParams,
- WorkspaceTarget,
+ AgentStatus, Method, OutputMatch, PaneAgentState, PaneWaitForOutputParams, ReadFormat,
+ ReadSource, Request, SplitDirection, Subscription,
};
+mod agent;
+mod integration;
+mod pane;
+mod server;
+mod status;
+mod tab;
+mod workspace;
+
pub enum CommandOutcome {
Handled(i32),
NotCli,
@@ -27,20 +28,20 @@ pub fn maybe_run(args: &[String]) -> std::io::Result<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]");