1131 lines
34 KiB
Rust
1131 lines
34 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, Method, OutputMatch, PaneListParams, PaneReadParams,
|
|
PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams, PaneTarget,
|
|
PaneWaitForOutputParams, 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 {
|
|
"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..])?,
|
|
_ => return Ok(CommandOutcome::NotCli),
|
|
};
|
|
|
|
Ok(CommandOutcome::Handled(exit_code))
|
|
}
|
|
|
|
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..]),
|
|
_ => {
|
|
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..]),
|
|
_ => {
|
|
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..]),
|
|
_ => {
|
|
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..]),
|
|
_ => {
|
|
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..]),
|
|
_ => {
|
|
print_integration_help();
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
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) = args.first().map(|arg| arg.as_str()) else {
|
|
eprintln!("usage: herdr integration install <pi|claude|codex|opencode>");
|
|
return Ok(2);
|
|
};
|
|
if args.len() != 1 {
|
|
eprintln!("usage: herdr integration install <pi|claude|codex|opencode>");
|
|
return Ok(2);
|
|
}
|
|
|
|
match target {
|
|
"pi" => {
|
|
let path = crate::integration::install_pi()?;
|
|
println!("installed pi integration to {}", path.display());
|
|
Ok(0)
|
|
}
|
|
"claude" => {
|
|
let installed = crate::integration::install_claude()?;
|
|
println!(
|
|
"installed claude integration hook to {}",
|
|
installed.hook_path.display()
|
|
);
|
|
println!(
|
|
"ensured claude settings at {}",
|
|
installed.settings_path.display()
|
|
);
|
|
Ok(0)
|
|
}
|
|
"codex" => {
|
|
let installed = crate::integration::install_codex()?;
|
|
println!(
|
|
"installed codex integration hook to {}",
|
|
installed.hook_path.display()
|
|
);
|
|
println!("ensured codex hooks at {}", installed.hooks_path.display());
|
|
println!(
|
|
"ensured codex config at {}",
|
|
installed.config_path.display()
|
|
);
|
|
Ok(0)
|
|
}
|
|
"opencode" => {
|
|
let installed = crate::integration::install_opencode()?;
|
|
println!(
|
|
"installed opencode integration plugin to {}",
|
|
installed.plugin_path.display()
|
|
);
|
|
Ok(0)
|
|
}
|
|
_ => {
|
|
eprintln!("unknown integration target: {target}");
|
|
eprintln!("currently supported: pi, claude, codex, opencode");
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn integration_uninstall(args: &[String]) -> std::io::Result<i32> {
|
|
let Some(target) = args.first().map(|arg| arg.as_str()) else {
|
|
eprintln!("usage: herdr integration uninstall <pi|claude|codex|opencode>");
|
|
return Ok(2);
|
|
};
|
|
if args.len() != 1 {
|
|
eprintln!("usage: herdr integration uninstall <pi|claude|codex|opencode>");
|
|
return Ok(2);
|
|
}
|
|
|
|
match target {
|
|
"pi" => {
|
|
let result = crate::integration::uninstall_pi()?;
|
|
if result.removed_extension {
|
|
println!(
|
|
"removed pi integration extension at {}",
|
|
result.extension_path.display()
|
|
);
|
|
} else {
|
|
println!(
|
|
"no pi integration extension found at {}",
|
|
result.extension_path.display()
|
|
);
|
|
}
|
|
Ok(0)
|
|
}
|
|
"claude" => {
|
|
let result = crate::integration::uninstall_claude()?;
|
|
if result.removed_hook_file {
|
|
println!("removed claude hook at {}", result.hook_path.display());
|
|
} else {
|
|
println!("no claude hook found at {}", result.hook_path.display());
|
|
}
|
|
if result.updated_settings {
|
|
println!(
|
|
"removed herdr claude hook entries from {}",
|
|
result.settings_path.display()
|
|
);
|
|
} else {
|
|
println!(
|
|
"no herdr claude hook entries found in {}",
|
|
result.settings_path.display()
|
|
);
|
|
}
|
|
Ok(0)
|
|
}
|
|
"codex" => {
|
|
let result = crate::integration::uninstall_codex()?;
|
|
if result.removed_hook_file {
|
|
println!("removed codex hook at {}", result.hook_path.display());
|
|
} else {
|
|
println!("no codex hook found at {}", result.hook_path.display());
|
|
}
|
|
if result.updated_hooks {
|
|
println!(
|
|
"removed herdr codex hook entries from {}",
|
|
result.hooks_path.display()
|
|
);
|
|
} else {
|
|
println!(
|
|
"no herdr codex hook entries found in {}",
|
|
result.hooks_path.display()
|
|
);
|
|
}
|
|
println!(
|
|
"left codex config unchanged at {}",
|
|
result.config_path.display()
|
|
);
|
|
Ok(0)
|
|
}
|
|
"opencode" => {
|
|
let result = crate::integration::uninstall_opencode()?;
|
|
if result.removed_plugin {
|
|
println!(
|
|
"removed opencode integration plugin at {}",
|
|
result.plugin_path.display()
|
|
);
|
|
} else {
|
|
println!(
|
|
"no opencode integration plugin found at {}",
|
|
result.plugin_path.display()
|
|
);
|
|
}
|
|
Ok(0)
|
|
}
|
|
_ => {
|
|
eprintln!("unknown integration target: {target}");
|
|
eprintln!("currently supported: pi, claude, codex, opencode");
|
|
Ok(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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]");
|
|
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]");
|
|
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_json<T: Serialize>(value: &T) {
|
|
println!("{}", serde_json::to_string(value).unwrap());
|
|
}
|