use std::io::{self, Write};
use bitrouter_config::BitrouterConfig;
use bitrouter_providers::acp::ops::{self, AcpPaths, AgentList, RoutingCheck};
use bitrouter_providers::acp::types::InstallProgress;
use crate::cli::OutputFormat;
use crate::runtime::paths::RuntimePaths;
fn paths_from_runtime(paths: &RuntimePaths) -> AcpPaths {
AcpPaths {
cache_dir: paths.cache_dir.clone(),
agents_dir: paths.agents_dir.clone(),
agent_state_file: paths.agent_state_file.clone(),
}
}
pub fn render_list_text(list: &AgentList, w: &mut impl Write) -> io::Result<()> {
if list.agents.is_empty() {
writeln!(w, " (no agents configured)")?;
return Ok(());
}
for warning in &list.warnings {
eprintln!(" warning: {warning}");
}
eprintln!();
eprintln!(" Agents");
eprintln!(" ──────");
eprintln!();
for info in &list.agents {
let status = match (&info.installed, info.on_path) {
(Some(rec), _) => format!("\u{2713} installed ({})", rec.method),
(None, true) => "\u{2713} on PATH".to_owned(),
(None, false) => "\u{2717} not installed".to_owned(),
};
let version = info.version.as_deref().unwrap_or("-");
writeln!(w, " {:<20} {:<10} {status}", info.id, version)?;
}
writeln!(w)?;
Ok(())
}
pub async fn run_list(
config: &BitrouterConfig,
paths: &RuntimePaths,
refresh: bool,
output: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let acp_paths = paths_from_runtime(paths);
let list = ops::list_agents(config, &acp_paths, refresh).await?;
match output {
OutputFormat::Text => render_list_text(&list, &mut io::stdout())?,
OutputFormat::Json => serde_json::to_writer(io::stdout(), &list)?,
}
Ok(())
}
pub async fn run_install(
agent_id: &str,
config: &BitrouterConfig,
paths: &RuntimePaths,
) -> Result<(), Box<dyn std::error::Error>> {
let acp_paths = paths_from_runtime(paths);
let mut handle = ops::install_agent(agent_id, config, &acp_paths);
let id = agent_id.to_owned();
while let Some(p) = handle.progress.recv().await {
match p {
InstallProgress::Downloading {
bytes_received,
total,
} => {
if let Some(t) = total {
let pct = bytes_received
.saturating_mul(100)
.checked_div(t)
.unwrap_or(0);
eprintln!(" [{id}] downloading: {pct}%");
} else {
eprintln!(" [{id}] downloading...");
}
}
InstallProgress::Extracting => eprintln!(" [{id}] extracting..."),
InstallProgress::Done(path) => eprintln!(" [{id}] done: {}", path.display()),
InstallProgress::Failed(msg) => eprintln!(" [{id}] failed: {msg}"),
}
}
let installed = handle.result.await??;
println!(
" \u{2713} {} installed via {}",
installed.agent_id, installed.method
);
Ok(())
}
pub async fn run_uninstall(
agent_id: &str,
paths: &RuntimePaths,
) -> Result<(), Box<dyn std::error::Error>> {
let acp_paths = paths_from_runtime(paths);
ops::uninstall_agent(agent_id, &acp_paths).await?;
println!(" \u{2713} {agent_id} uninstalled");
Ok(())
}
pub async fn run_update(
agent_id: Option<&str>,
config: &BitrouterConfig,
paths: &RuntimePaths,
) -> Result<(), Box<dyn std::error::Error>> {
let acp_paths = paths_from_runtime(paths);
let records = bitrouter_providers::acp::state::load_state_sync(&paths.agent_state_file);
if records.is_empty() {
println!(" (no agents installed)");
return Ok(());
}
let targets: Vec<String> = match agent_id {
Some(id) => {
if !records.iter().any(|r| r.id == id) {
return Err(format!("agent '{id}' is not installed").into());
}
vec![id.to_owned()]
}
None => records.iter().map(|r| r.id.clone()).collect(),
};
for id in &targets {
eprintln!(" → updating {id}");
let mut handle = ops::install_agent(id, config, &acp_paths);
while let Some(p) = handle.progress.recv().await {
match p {
InstallProgress::Downloading {
bytes_received,
total,
} => {
if let Some(t) = total {
let pct = bytes_received
.saturating_mul(100)
.checked_div(t)
.unwrap_or(0);
eprintln!(" [{id}] downloading: {pct}%");
} else {
eprintln!(" [{id}] downloading...");
}
}
InstallProgress::Extracting => eprintln!(" [{id}] extracting..."),
InstallProgress::Done(path) => eprintln!(" [{id}] done: {}", path.display()),
InstallProgress::Failed(msg) => eprintln!(" [{id}] failed: {msg}"),
}
}
let installed = handle.result.await??;
println!(
" \u{2713} {} installed via {}",
installed.agent_id, installed.method
);
}
Ok(())
}
pub fn render_check_text(check: &RoutingCheck, w: &mut impl Write) -> io::Result<()> {
eprintln!();
eprintln!(" Agent Routing Check");
eprintln!(" ───────────────────");
eprintln!();
if check.shim_entries.is_empty() {
eprintln!(" (no agents with a known routing mapping discovered)");
} else {
let mut needs_shim: Vec<&str> = Vec::new();
for entry in &check.shim_entries {
if entry.shim_installed {
eprintln!(
" \u{2713} {} shim \u{2192} {}",
entry.agent_id,
entry.shim_path.display()
);
} else {
eprintln!(" \u{2717} {} shim not installed", entry.agent_id);
needs_shim.push(&entry.agent_id);
}
}
if !needs_shim.is_empty() {
eprintln!();
eprintln!(
" Run `bitrouter init` to install shims for: {}",
needs_shim.join(", ")
);
}
}
eprintln!();
if check.server_reachable {
writeln!(w, " \u{2713} BitRouter reachable at {}", check.listen_addr)?;
} else {
writeln!(
w,
" \u{2717} BitRouter not reachable at {}",
check.listen_addr
)?;
writeln!(w, " Start the server: bitrouter serve")?;
}
eprintln!();
if check.discovered_on_path.is_empty() && check.discovered_distributable.is_empty() {
eprintln!(" \u{2717} No ACP agents discovered");
} else {
if !check.discovered_on_path.is_empty() {
eprintln!(
" \u{2713} Agents on PATH: {}",
check.discovered_on_path.join(", ")
);
}
if !check.discovered_distributable.is_empty() {
eprintln!(
" \u{2713} Agents available (auto-install): {}",
check.discovered_distributable.join(", ")
);
}
}
eprintln!();
Ok(())
}
pub fn run_check(
config: &BitrouterConfig,
output: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let check = ops::check_routing(config);
match output {
OutputFormat::Text => render_check_text(&check, &mut io::stdout())?,
OutputFormat::Json => serde_json::to_writer(io::stdout(), &check)?,
}
Ok(())
}