use anyhow::Result;
use rch_common::ApiResponse;
use crate::ui::context::OutputContext;
use crate::ui::theme::StatusIndicator;
pub fn agents_list(all: bool, ctx: &OutputContext) -> Result<()> {
use crate::agent::{AgentKind, HookStatus, check_hook_status, detect_agents};
let style = ctx.theme();
let detected = detect_agents()?;
let mut agent_infos: Vec<serde_json::Value> = Vec::new();
let agents_to_show: Vec<AgentKind> = if all {
AgentKind::ALL.to_vec()
} else {
detected.iter().map(|a| a.kind).collect()
};
for kind in &agents_to_show {
let detected_info = detected.iter().find(|a| a.kind == *kind);
let is_detected = detected_info.is_some();
let hook_status = check_hook_status(*kind).unwrap_or(HookStatus::NotSupported);
let info = serde_json::json!({
"name": kind.name(),
"kind": format!("{:?}", kind),
"detected": is_detected,
"version": detected_info.and_then(|d| d.version.clone()),
"config_path": detected_info.and_then(|d| d.config_path.as_ref().map(|p| p.display().to_string())),
"hook_support": format!("{}", kind.hook_support()),
"hook_status": hook_status.to_string(),
"can_install_hook": kind.hook_support().can_install_hook(),
});
agent_infos.push(info);
}
if ctx.is_json() {
let _ = ctx.json(&ApiResponse::ok(
"agents list",
serde_json::json!({
"agents": agent_infos,
"total": agent_infos.len(),
"detected": detected.len(),
}),
));
return Ok(());
}
println!("{}", style.format_header("Detected AI Coding Agents"));
println!();
if agents_to_show.is_empty() {
println!(" {} No AI coding agents detected.", style.muted("ℹ"));
println!();
println!(" Run with --all to see all known agents.");
return Ok(());
}
for kind in &agents_to_show {
let detected_info = detected.iter().find(|a| a.kind == *kind);
let is_detected = detected_info.is_some();
let hook_status = check_hook_status(*kind).unwrap_or(HookStatus::NotSupported);
let status_indicator = if is_detected {
match hook_status {
HookStatus::Installed => StatusIndicator::Success,
HookStatus::NeedsUpdate => StatusIndicator::Warning,
HookStatus::NotInstalled => StatusIndicator::Info,
HookStatus::NotSupported => StatusIndicator::Pending,
}
} else {
StatusIndicator::Pending
};
let detected_label = if is_detected {
style.success("detected")
} else {
style.muted("not found")
};
println!(
" {} {} {}",
status_indicator.display(style),
style.key(kind.name()),
detected_label
);
if let Some(info) = detected_info {
if let Some(ref version) = info.version {
println!(" {} {}", style.muted("Version:"), style.value(version));
}
if let Some(ref path) = info.config_path {
println!(
" {} {}",
style.muted("Config:"),
style.value(&path.display().to_string())
);
}
}
println!(
" {} {}",
style.muted("Hook:"),
match hook_status {
HookStatus::Installed => style.success(&hook_status.to_string()),
HookStatus::NeedsUpdate => style.warning(&hook_status.to_string()),
HookStatus::NotInstalled => style.value(&hook_status.to_string()),
HookStatus::NotSupported => style.muted(&hook_status.to_string()),
}
);
println!();
}
Ok(())
}
pub fn agents_status(agent: Option<String>, ctx: &OutputContext) -> Result<()> {
use crate::agent::{AgentKind, HookStatus, check_hook_status, detect_single_agent};
let style = ctx.theme();
if let Some(ref agent_name) = agent {
let kind = AgentKind::from_id(agent_name).ok_or_else(|| {
anyhow::anyhow!(
"Unknown agent: {}. Valid agents: {:?}",
agent_name,
AgentKind::ALL.iter().map(|k| k.id()).collect::<Vec<_>>()
)
})?;
let detected = detect_single_agent(kind)?;
let hook_status = check_hook_status(kind).unwrap_or(HookStatus::NotSupported);
let info = serde_json::json!({
"name": kind.name(),
"kind": format!("{:?}", kind),
"detected": detected.is_some(),
"version": detected.as_ref().and_then(|d| d.version.clone()),
"config_path": detected.as_ref().and_then(|d| d.config_path.as_ref().map(|p| p.display().to_string())),
"detection_method": detected.as_ref().map(|d| format!("{:?}", d.detection_method)),
"hook_support": format!("{}", kind.hook_support()),
"hook_status": hook_status.to_string(),
"can_install_hook": kind.hook_support().can_install_hook(),
});
if ctx.is_json() {
let _ = ctx.json(&ApiResponse::ok("agents status", info));
return Ok(());
}
println!(
"{}",
style.format_header(&format!("Agent: {}", kind.name()))
);
println!();
println!(
" {} {}",
style.key("Detected:"),
if detected.is_some() {
style.success("Yes")
} else {
style.warning("No")
}
);
if let Some(ref d) = detected {
if let Some(ref version) = d.version {
println!(" {} {}", style.key("Version:"), style.value(version));
}
if let Some(ref path) = d.config_path {
println!(
" {} {}",
style.key("Config:"),
style.value(&path.display().to_string())
);
}
println!(" {} {:?}", style.key("Detection:"), d.detection_method);
}
println!(
" {} {}",
style.key("Hook Support:"),
style.value(&format!("{}", kind.hook_support()))
);
println!(
" {} {}",
style.key("Hook Status:"),
match hook_status {
HookStatus::Installed => style.success(&hook_status.to_string()),
HookStatus::NeedsUpdate => style.warning(&hook_status.to_string()),
HookStatus::NotInstalled => style.value(&hook_status.to_string()),
HookStatus::NotSupported => style.muted(&hook_status.to_string()),
}
);
return Ok(());
}
agents_list(false, ctx)
}
pub fn agents_install_hook(agent: &str, dry_run: bool, ctx: &OutputContext) -> Result<()> {
use crate::agent::{AgentKind, install_hook};
use crate::state::primitives::IdempotentResult;
let style = ctx.theme();
let kind = AgentKind::from_id(agent).ok_or_else(|| {
anyhow::anyhow!(
"Unknown agent '{}'. Supported agents: {}",
agent,
AgentKind::ALL
.iter()
.map(|k| k.id())
.collect::<Vec<_>>()
.join(", ")
)
})?;
if !ctx.is_json() {
if dry_run {
println!("Dry run: checking hook installation for {}...", kind.name());
} else {
println!("Installing RCH hook for {}...", kind.name());
}
}
let result = install_hook(kind, dry_run)?;
if ctx.is_json() {
let _ = ctx.json(&serde_json::json!({
"agent": kind.id(),
"action": "install",
"result": format!("{:?}", result),
"dry_run": dry_run
}));
} else {
match result {
IdempotentResult::Changed => {
println!(
"{} RCH hook installed for {}",
StatusIndicator::Success.display(style),
kind.name()
);
}
IdempotentResult::Unchanged => {
println!(
"{} RCH hook already installed for {}",
StatusIndicator::Success.display(style),
kind.name()
);
}
IdempotentResult::WouldChange(msg) => {
println!("{} {}", StatusIndicator::Info.display(style), msg);
}
IdempotentResult::NotApplicable(msg) => {
println!("{} {}", StatusIndicator::Warning.display(style), msg);
}
other => {
println!(
"{} Hook installation result: {:?}",
StatusIndicator::Info.display(style),
other
);
}
}
}
Ok(())
}
pub fn agents_uninstall_hook(agent: &str, dry_run: bool, ctx: &OutputContext) -> Result<()> {
use crate::agent::{AgentKind, uninstall_hook};
use crate::state::primitives::IdempotentResult;
let style = ctx.theme();
let kind = AgentKind::from_id(agent).ok_or_else(|| {
anyhow::anyhow!(
"Unknown agent '{}'. Supported agents: {}",
agent,
AgentKind::ALL
.iter()
.map(|k| k.id())
.collect::<Vec<_>>()
.join(", ")
)
})?;
if !ctx.is_json() {
if dry_run {
println!("Dry run: checking hook removal for {}...", kind.name());
} else {
println!("Uninstalling RCH hook from {}...", kind.name());
}
}
let result = uninstall_hook(kind, dry_run)?;
if ctx.is_json() {
let _ = ctx.json(&serde_json::json!({
"agent": kind.id(),
"action": "uninstall",
"result": format!("{:?}", result),
"dry_run": dry_run
}));
} else {
match result {
IdempotentResult::Changed => {
println!(
"{} RCH hook removed from {}",
StatusIndicator::Success.display(style),
kind.name()
);
}
IdempotentResult::Unchanged => {
println!(
"{} RCH hook was not installed for {}",
StatusIndicator::Info.display(style),
kind.name()
);
}
IdempotentResult::WouldChange(msg) => {
println!("{} {}", StatusIndicator::Info.display(style), msg);
}
IdempotentResult::NotApplicable(msg) => {
println!("{} {}", StatusIndicator::Warning.display(style), msg);
}
other => {
println!(
"{} Hook uninstallation result: {:?}",
StatusIndicator::Info.display(style),
other
);
}
}
}
Ok(())
}