mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
1469 lines
43 KiB
Rust
1469 lines
43 KiB
Rust
use std::io::{BufRead, BufReader, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::time::Duration;
|
|
|
|
use serde::Serialize;
|
|
|
|
use crate::api;
|
|
use crate::api::schema::{
|
|
AgentStatus, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneListParams,
|
|
PaneReadParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams,
|
|
PaneTarget, PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection,
|
|
Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget,
|
|
WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget,
|
|
};
|
|
|
|
pub enum CommandOutcome {
|
|
Handled(i32),
|
|
NotCli,
|
|
}
|
|
|
|
pub fn maybe_run(args: &[String]) -> std::io::Result<CommandOutcome> {
|
|
let Some(command) = args.get(1).map(|arg| arg.as_str()) else {
|
|
return Ok(CommandOutcome::NotCli);
|
|
};
|
|
|
|
let exit_code = match command {
|
|
"server" => {
|
|
let Some(exit_code) = run_server_command(&args[2..])? else {
|
|
return Ok(CommandOutcome::NotCli);
|
|
};
|
|
exit_code
|
|
}
|
|
"status" => run_status_command(&args[2..])?,
|
|
"workspace" => run_workspace_command(&args[2..])?,
|
|
"tab" => run_tab_command(&args[2..])?,
|
|
"pane" => run_pane_command(&args[2..])?,
|
|
"wait" => run_wait_command(&args[2..])?,
|
|
"integration" => run_integration_command(&args[2..])?,
|
|
"session" => run_session_command(&args[2..])?,
|
|
_ => return Ok(CommandOutcome::NotCli),
|
|
};
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
enum ServerRuntimeStatus {
|
|
Running {
|
|
version: Option<String>,
|
|
protocol: Option<u32>,
|
|
},
|
|
NotRunning,
|
|
}
|
|
|
|
fn print_full_status() -> std::io::Result<i32> {
|
|
let server = read_server_runtime_status()?;
|
|
|
|
println!("client:");
|
|
println!(" version: {}", env!("CARGO_PKG_VERSION"));
|
|
println!(" protocol: {}", crate::server::protocol::PROTOCOL_VERSION);
|
|
println!();
|
|
println!("server:");
|
|
print_server_status_body(&server, " ");
|
|
println!();
|
|
println!("update:");
|
|
println!(" restart_needed: {}", restart_needed_label(&server));
|
|
|
|
Ok(0)
|
|
}
|
|
|
|
fn print_server_status() -> std::io::Result<i32> {
|
|
let server = read_server_runtime_status()?;
|
|
print_server_status_body(&server, "");
|
|
Ok(0)
|
|
}
|
|
|
|
fn print_client_status() {
|
|
println!("version: {}", env!("CARGO_PKG_VERSION"));
|
|
println!("protocol: {}", crate::server::protocol::PROTOCOL_VERSION);
|
|
println!("binary: {}", current_exe_label());
|
|
}
|
|
|
|
fn print_server_status_body(server: &ServerRuntimeStatus, indent: &str) {
|
|
match server {
|
|
ServerRuntimeStatus::Running { version, protocol } => {
|
|
println!("{indent}status: running");
|
|
println!("{indent}version: {}", option_label(version.as_deref()));
|
|
println!("{indent}protocol: {}", protocol_label(*protocol));
|
|
println!("{indent}compatible: {}", compatibility_label(*protocol));
|
|
println!("{indent}socket: {}", api::socket_path().display());
|
|
}
|
|
ServerRuntimeStatus::NotRunning => {
|
|
println!("{indent}status: not running");
|
|
println!("{indent}socket: {}", api::socket_path().display());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn read_server_runtime_status() -> std::io::Result<ServerRuntimeStatus> {
|
|
match send_request(&Request {
|
|
id: "cli:status:server".into(),
|
|
method: Method::Ping(PingParams::default()),
|
|
}) {
|
|
Ok(response) => {
|
|
if response.get("error").is_some() {
|
|
return Err(std::io::Error::other(format!(
|
|
"server status request failed: {}",
|
|
response
|
|
)));
|
|
}
|
|
|
|
let result = &response["result"];
|
|
Ok(ServerRuntimeStatus::Running {
|
|
version: result
|
|
.get("version")
|
|
.and_then(|value| value.as_str())
|
|
.map(str::to_owned),
|
|
protocol: result
|
|
.get("protocol")
|
|
.and_then(|value| value.as_u64())
|
|
.and_then(|value| u32::try_from(value).ok()),
|
|
})
|
|
}
|
|
Err(err) if server_not_running_error(&err) => Ok(ServerRuntimeStatus::NotRunning),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn server_not_running_error(err: &std::io::Error) -> bool {
|
|
matches!(
|
|
err.kind(),
|
|
std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
|
|
)
|
|
}
|
|
|
|
fn option_label(value: Option<&str>) -> &str {
|
|
value.unwrap_or("unknown")
|
|
}
|
|
|
|
fn protocol_label(protocol: Option<u32>) -> String {
|
|
protocol
|
|
.map(|value| value.to_string())
|
|
.unwrap_or_else(|| "unknown".to_string())
|
|
}
|
|
|
|
fn compatibility_label(protocol: Option<u32>) -> &'static str {
|
|
match protocol {
|
|
Some(protocol) if protocol == crate::server::protocol::PROTOCOL_VERSION => "yes",
|
|
Some(_) => "no",
|
|
None => "unknown",
|
|
}
|
|
}
|
|
|
|
fn restart_needed_label(server: &ServerRuntimeStatus) -> &'static str {
|
|
match server {
|
|
ServerRuntimeStatus::Running { version, .. } => match version.as_deref() {
|
|
Some(version) if version == env!("CARGO_PKG_VERSION") => "no",
|
|
Some(_) => "yes",
|
|
None => "unknown",
|
|
},
|
|
ServerRuntimeStatus::NotRunning => "no",
|
|
}
|
|
}
|
|
|
|
fn current_exe_label() -> String {
|
|
std::env::current_exe()
|
|
.map(|path| path.display().to_string())
|
|
.unwrap_or_else(|err| format!("unknown ({err})"))
|
|
}
|
|
|
|
fn run_workspace_command(args: &[String]) -> std::io::Result<i32> {
|
|
let Some(subcommand) = args.first().map(|arg| arg.as_str()) else {
|
|
print_workspace_help();
|
|
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_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..]),
|
|
"split" => pane_split(&args[1..]),
|
|
"close" => pane_close(&args[1..]),
|
|
"send-text" => pane_send_text(&args[1..]),
|
|
"send-keys" => pane_send_keys(&args[1..]),
|
|
"run" => pane_run(&args[1..]),
|
|
"help" | "--help" | "-h" => {
|
|
print_pane_help();
|
|
Ok(0)
|
|
}
|
|
_ => {
|
|
print_pane_help();
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_wait_command(args: &[String]) -> std::io::Result<i32> {
|
|
let Some(subcommand) = args.first().map(|arg| arg.as_str()) else {
|
|
print_wait_help();
|
|
return Ok(2);
|
|
};
|
|
|
|
match subcommand {
|
|
"output" => wait_output(&args[1..]),
|
|
"agent-status" => wait_agent_status(&args[1..]),
|
|
"help" | "--help" | "-h" => {
|
|
print_wait_help();
|
|
Ok(0)
|
|
}
|
|
_ => {
|
|
print_wait_help();
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_integration_command(args: &[String]) -> std::io::Result<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..]),
|
|
"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();
|
|
return Ok(2);
|
|
};
|
|
|
|
match subcommand {
|
|
"list" => session_list(&args[1..]),
|
|
"attach" => session_attach_help(&args[1..]),
|
|
"stop" => session_stop(&args[1..]),
|
|
"delete" => session_delete(&args[1..]),
|
|
"help" | "--help" | "-h" => {
|
|
print_session_help();
|
|
Ok(0)
|
|
}
|
|
_ => {
|
|
print_session_help();
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn server_stop(args: &[String]) -> std::io::Result<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),
|
|
Some("help" | "--help" | "-h")
|
|
) {
|
|
eprintln!("usage: herdr session attach <name>");
|
|
return Ok(0);
|
|
}
|
|
eprintln!("usage: herdr session attach <name>");
|
|
Ok(2)
|
|
}
|
|
|
|
fn session_list(args: &[String]) -> std::io::Result<i32> {
|
|
let json = match parse_session_json_only(args, "usage: herdr session list [--json]") {
|
|
Ok(json) => json,
|
|
Err(code) => return Ok(code),
|
|
};
|
|
|
|
let sessions = crate::session::list_sessions()?;
|
|
if json {
|
|
_print_json(&serde_json::json!({
|
|
"sessions": sessions,
|
|
}));
|
|
} else {
|
|
print_session_table(&sessions);
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
fn session_stop(args: &[String]) -> std::io::Result<i32> {
|
|
let (name, json) =
|
|
match parse_session_name_and_json(args, "usage: herdr session stop <name> [--json]") {
|
|
Ok(parsed) => parsed,
|
|
Err(code) => return Ok(code),
|
|
};
|
|
|
|
let target = match crate::session::parse_target_name(&name) {
|
|
Ok(target) => target,
|
|
Err(message) => {
|
|
print_session_error("invalid_session_name", &message);
|
|
return Ok(1);
|
|
}
|
|
};
|
|
match crate::session::stop_session(target.as_deref()) {
|
|
Ok(session) => {
|
|
if json {
|
|
_print_json(&serde_json::json!({
|
|
"stopped": true,
|
|
"session": session,
|
|
}));
|
|
} else {
|
|
println!("stopped session {}", session.name);
|
|
}
|
|
Ok(0)
|
|
}
|
|
Err(message) => {
|
|
print_session_error("session_stop_failed", &message);
|
|
Ok(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn session_delete(args: &[String]) -> std::io::Result<i32> {
|
|
let (name, json) =
|
|
match parse_session_name_and_json(args, "usage: herdr session delete <name> [--json]") {
|
|
Ok(parsed) => parsed,
|
|
Err(code) => return Ok(code),
|
|
};
|
|
|
|
match crate::session::delete_session(&name) {
|
|
Ok(session) => {
|
|
if json {
|
|
_print_json(&serde_json::json!({
|
|
"deleted": true,
|
|
"session": session,
|
|
}));
|
|
} else {
|
|
println!("deleted session {}", session.name);
|
|
}
|
|
Ok(0)
|
|
}
|
|
Err(message) => {
|
|
print_session_error("session_delete_failed", &message);
|
|
Ok(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn workspace_list(args: &[String]) -> std::io::Result<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 = true;
|
|
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;
|
|
}
|
|
"--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 = true;
|
|
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;
|
|
}
|
|
"--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 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_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]");
|
|
return Ok(2);
|
|
};
|
|
|
|
let pane_id = normalize_pane_id(raw_pane_id);
|
|
let mut source = ReadSource::Recent;
|
|
let mut lines = None;
|
|
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;
|
|
}
|
|
"--raw" => {
|
|
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,
|
|
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] [--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 = true;
|
|
|
|
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;
|
|
}
|
|
"--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 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>");
|
|
return Ok(None);
|
|
};
|
|
if args.len() != 1 {
|
|
eprintln!("usage: herdr integration {action} <pi|claude|codex|opencode>");
|
|
return Ok(None);
|
|
}
|
|
|
|
let parsed = match target {
|
|
"pi" => IntegrationTarget::Pi,
|
|
"claude" => IntegrationTarget::Claude,
|
|
"codex" => IntegrationTarget::Codex,
|
|
"opencode" => IntegrationTarget::Opencode,
|
|
_ => {
|
|
eprintln!("unknown integration target: {target}");
|
|
eprintln!("currently supported: pi, claude, codex, opencode");
|
|
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]");
|
|
return Ok(2);
|
|
};
|
|
|
|
let pane_id = normalize_pane_id(raw_pane_id);
|
|
let mut source = ReadSource::Recent;
|
|
let mut lines = None;
|
|
let mut timeout_ms = None;
|
|
let mut strip_ansi = true;
|
|
let mut regex = false;
|
|
let mut match_value = None;
|
|
|
|
let mut index = 1;
|
|
while index < args.len() {
|
|
match args[index].as_str() {
|
|
"--match" => {
|
|
let Some(value) = args.get(index + 1) else {
|
|
eprintln!("missing value for --match");
|
|
return Ok(2);
|
|
};
|
|
match_value = Some(value.clone());
|
|
index += 2;
|
|
}
|
|
"--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;
|
|
}
|
|
"--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;
|
|
}
|
|
"--regex" => {
|
|
regex = true;
|
|
index += 1;
|
|
}
|
|
"--raw" => {
|
|
strip_ansi = false;
|
|
index += 1;
|
|
}
|
|
other => {
|
|
eprintln!("unknown option: {other}");
|
|
return Ok(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
let Some(match_value) = match_value else {
|
|
eprintln!("missing required --match");
|
|
return Ok(2);
|
|
};
|
|
|
|
let matcher = if regex {
|
|
OutputMatch::Regex { value: match_value }
|
|
} else {
|
|
OutputMatch::Substring { value: match_value }
|
|
};
|
|
|
|
let response = send_request(&Request {
|
|
id: "cli:wait:output".into(),
|
|
method: Method::PaneWaitForOutput(PaneWaitForOutputParams {
|
|
pane_id,
|
|
source,
|
|
lines,
|
|
r#match: matcher,
|
|
timeout_ms,
|
|
strip_ansi,
|
|
}),
|
|
})?;
|
|
|
|
if response.get("error").is_some() {
|
|
eprintln!("{}", serde_json::to_string(&response).unwrap());
|
|
return Ok(1);
|
|
}
|
|
|
|
println!("{}", serde_json::to_string(&response).unwrap());
|
|
Ok(0)
|
|
}
|
|
|
|
fn wait_agent_status(args: &[String]) -> std::io::Result<i32> {
|
|
let Some(raw_pane_id) = args.first() else {
|
|
eprintln!("usage: herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]");
|
|
return Ok(2);
|
|
};
|
|
|
|
let pane_id = normalize_pane_id(raw_pane_id);
|
|
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_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;
|
|
}
|
|
other => {
|
|
eprintln!("unknown option: {other}");
|
|
return Ok(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
let Some(agent_status) = desired_status else {
|
|
eprintln!("missing required --status");
|
|
return Ok(2);
|
|
};
|
|
|
|
wait_for_agent_change(
|
|
Request {
|
|
id: "cli:wait:agent-status".into(),
|
|
method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams {
|
|
subscriptions: vec![Subscription::PaneAgentStatusChanged {
|
|
pane_id,
|
|
agent_status: Some(agent_status),
|
|
}],
|
|
}),
|
|
},
|
|
timeout_ms,
|
|
"timed out waiting for agent status change",
|
|
)
|
|
}
|
|
|
|
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 mut event = String::new();
|
|
match reader.read_line(&mut event) {
|
|
Ok(0) => {
|
|
eprintln!("subscription closed before event arrived");
|
|
Ok(1)
|
|
}
|
|
Ok(_) => {
|
|
let event_value: serde_json::Value = serde_json::from_str(&event)?;
|
|
println!("{}", serde_json::to_string(&event_value).unwrap());
|
|
Ok(0)
|
|
}
|
|
Err(err)
|
|
if matches!(
|
|
err.kind(),
|
|
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
|
|
) =>
|
|
{
|
|
eprintln!("{timeout_message}");
|
|
Ok(1)
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
println!("{}", serde_json::to_string(response).unwrap());
|
|
Ok(0)
|
|
}
|
|
|
|
fn send_ok_request(method: Method) -> std::io::Result<i32> {
|
|
let response = send_request(&Request {
|
|
id: "cli:request".into(),
|
|
method,
|
|
})?;
|
|
|
|
if response.get("error").is_some() {
|
|
eprintln!("{}", serde_json::to_string(&response).unwrap());
|
|
return Ok(1);
|
|
}
|
|
|
|
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()?;
|
|
|
|
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 normalize_workspace_id(value: &str) -> String {
|
|
value.to_string()
|
|
}
|
|
|
|
fn normalize_tab_id(value: &str) -> String {
|
|
value.to_string()
|
|
}
|
|
|
|
fn normalize_pane_id(value: &str) -> String {
|
|
value.to_string()
|
|
}
|
|
|
|
fn parse_split_direction(value: &str) -> std::io::Result<SplitDirection> {
|
|
match value {
|
|
"right" => Ok(SplitDirection::Right),
|
|
"down" => Ok(SplitDirection::Down),
|
|
_ => Err(std::io::Error::other(format!(
|
|
"invalid split direction: {value}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn parse_read_source(value: &str) -> std::io::Result<ReadSource> {
|
|
match value {
|
|
"visible" => Ok(ReadSource::Visible),
|
|
"recent" => Ok(ReadSource::Recent),
|
|
"recent-unwrapped" | "recent_unwrapped" => Ok(ReadSource::RecentUnwrapped),
|
|
_ => Err(std::io::Error::other(format!(
|
|
"invalid read source: {value}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn parse_agent_status(value: &str) -> std::io::Result<AgentStatus> {
|
|
match value {
|
|
"idle" => Ok(AgentStatus::Idle),
|
|
"working" => Ok(AgentStatus::Working),
|
|
"blocked" => Ok(AgentStatus::Blocked),
|
|
"done" => Ok(AgentStatus::Done),
|
|
"unknown" => Ok(AgentStatus::Unknown),
|
|
_ => Err(std::io::Error::other(format!(
|
|
"invalid agent status: {value} (expected idle, working, blocked, done, or unknown)"
|
|
))),
|
|
}
|
|
}
|
|
|
|
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> {
|
|
value
|
|
.parse::<u64>()
|
|
.map_err(|_| std::io::Error::other(format!("invalid value for {flag}: {value}")))
|
|
}
|
|
|
|
fn parse_session_json_only(args: &[String], usage: &str) -> Result<bool, i32> {
|
|
match args {
|
|
[] => Ok(false),
|
|
[flag] if flag == "--json" => Ok(true),
|
|
_ => {
|
|
eprintln!("{usage}");
|
|
Err(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_session_name_and_json(args: &[String], usage: &str) -> Result<(String, bool), i32> {
|
|
let mut name = None;
|
|
let mut json = false;
|
|
for arg in args {
|
|
if arg == "--json" {
|
|
json = true;
|
|
} else if name.is_none() {
|
|
name = Some(arg.clone());
|
|
} else {
|
|
eprintln!("{usage}");
|
|
return Err(2);
|
|
}
|
|
}
|
|
|
|
let Some(name) = name else {
|
|
eprintln!("{usage}");
|
|
return Err(2);
|
|
};
|
|
Ok((name, json))
|
|
}
|
|
|
|
fn print_session_table(sessions: &[crate::session::SessionInfo]) {
|
|
println!(
|
|
"{:<20} {:<8} {:<48} {}",
|
|
"name", "status", "directory", "socket"
|
|
);
|
|
for session in sessions {
|
|
println!(
|
|
"{:<20} {:<8} {:<48} {}",
|
|
session.name,
|
|
if session.running {
|
|
"running"
|
|
} else {
|
|
"stopped"
|
|
},
|
|
session.session_dir,
|
|
session.socket_path
|
|
);
|
|
}
|
|
}
|
|
|
|
fn print_session_error(code: &str, message: &str) {
|
|
eprintln!(
|
|
"{}",
|
|
serde_json::to_string(&serde_json::json!({
|
|
"error": {
|
|
"code": code,
|
|
"message": message,
|
|
}
|
|
}))
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
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_workspace_help() {
|
|
eprintln!("herdr workspace commands:");
|
|
eprintln!(" herdr workspace list");
|
|
eprintln!(" herdr workspace create [--cwd PATH] [--label TEXT] [--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] [--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_pane_help() {
|
|
eprintln!("herdr pane commands:");
|
|
eprintln!(" herdr pane list [--workspace <workspace_id>]");
|
|
eprintln!(" herdr pane get <pane_id>");
|
|
eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]");
|
|
eprintln!(" herdr pane split <pane_id> --direction right|down [--cwd PATH] [--no-focus]");
|
|
eprintln!(" herdr pane close <pane_id>");
|
|
eprintln!(" herdr pane send-text <pane_id> <text>");
|
|
eprintln!(" herdr pane send-keys <pane_id> <key> [key ...]");
|
|
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]");
|
|
eprintln!(
|
|
" herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS]"
|
|
);
|
|
}
|
|
|
|
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 uninstall pi");
|
|
eprintln!(" herdr integration uninstall claude");
|
|
eprintln!(" herdr integration uninstall codex");
|
|
eprintln!(" herdr integration uninstall opencode");
|
|
}
|
|
|
|
fn print_session_help() {
|
|
eprintln!("herdr session commands:");
|
|
eprintln!(" herdr session list [--json]");
|
|
eprintln!(" herdr session attach <name>");
|
|
eprintln!(" herdr session stop <name> [--json]");
|
|
eprintln!(" herdr session delete <name> [--json]");
|
|
eprintln!(" use 'default' as <name> to target the default session for stop");
|
|
}
|
|
|
|
fn _print_json<T: Serialize>(value: &T) {
|
|
println!("{}", serde_json::to_string(value).unwrap());
|
|
}
|