use crate::agents::backend_for;
use crate::error::{Error, Result};
use crate::host::{AgentReport, AgentStatus, Desired, Outcome, Plugin, Scope, SkipReason, Source};
use crate::{lock, stamp};
pub(crate) fn install_report(plugin: &Plugin, scope: Scope, source: Source, filter: &[&str]) -> Result<AgentReport> {
let _lock = lock::acquire()?;
let desired = Desired { source, reenable: true };
reconcile_all(plugin, &desired, &scope, filter, false)
}
pub(crate) fn update_report(plugin: &Plugin, scope: Scope, source: Source) -> Result<AgentReport> {
let _lock = lock::acquire()?;
let desired = Desired { source, reenable: true };
let report = reconcile_all(plugin, &desired, &scope, &[], true)?;
if report.outcome_of("claude").is_some_and(|outcome| *outcome != Outcome::NoOp) {
let _ = crate::restart::set(plugin);
}
Ok(report)
}
pub(crate) fn uninstall_report(plugin: &Plugin, scope: Scope, default_source: Source) -> Result<AgentReport> {
let _lock = lock::acquire()?;
data_dir_precondition(plugin)?;
let mut report = AgentReport::new();
for id in plugin.agents {
report.push(id, uninstall_agent(plugin, &scope, id, &default_source));
}
Ok(report)
}
fn uninstall_agent(plugin: &Plugin, scope: &Scope, id: &'static str, default_source: &Source) -> AgentStatus {
let backend = match resolve(id) {
Ok(backend) => backend,
Err(e) => return AgentStatus::Failed(e.to_string()),
};
let source = stamp::resolve_source(plugin, scope, id, default_source.clone());
let status = if !backend.detect() {
AgentStatus::Skipped(SkipReason::NotDetected)
} else if !backend.capabilities().scopes.contains(&scope.as_cli()) {
AgentStatus::Skipped(SkipReason::ScopeUnsupported)
} else if matches!(source, Source::GitHub { .. }) && !backend.capabilities().plugins {
AgentStatus::Skipped(SkipReason::SourceUnsupported)
} else {
match backend.remove(plugin, scope, &source) {
Ok(outcome) => AgentStatus::Converged(outcome),
Err(e) => return AgentStatus::Failed(e.to_string()),
}
};
if let Err(e) = backend.forget(plugin, scope) {
return AgentStatus::Failed(e.to_string());
}
match stamp::clear(plugin, scope, id) {
Ok(()) => status,
Err(e) => AgentStatus::Failed(e.to_string()),
}
}
fn reconcile_all(plugin: &Plugin, desired: &Desired, scope: &Scope, filter: &[&str], rehydrate: bool) -> Result<AgentReport> {
data_dir_precondition(plugin)?;
let mut report = AgentReport::new();
for id in plugin.agents {
if !filter.is_empty() && !filter.contains(id) {
continue; }
report.push(id, reconcile_agent(plugin, desired, scope, id, rehydrate));
}
Ok(report)
}
fn reconcile_agent(plugin: &Plugin, desired: &Desired, scope: &Scope, id: &'static str, rehydrate: bool) -> AgentStatus {
let backend = match resolve(id) {
Ok(backend) => backend,
Err(e) => return AgentStatus::Failed(e.to_string()),
};
if !backend.detect() {
return AgentStatus::Skipped(SkipReason::NotDetected);
}
if !backend.capabilities().scopes.contains(&scope.as_cli()) {
return AgentStatus::Skipped(SkipReason::ScopeUnsupported);
}
let source = if rehydrate { stamp::resolve_source(plugin, scope, id, desired.source.clone()) } else { desired.source.clone() };
if matches!(source, Source::GitHub { .. }) && !backend.capabilities().plugins {
return AgentStatus::Skipped(SkipReason::SourceUnsupported);
}
let per_agent = Desired { source, reenable: desired.reenable };
let written = backend.reconcile(plugin, &per_agent, scope).and_then(|outcome| {
stamp::write(plugin, scope, &per_agent.source, id)?;
Ok(outcome)
});
match written {
Ok(outcome) => AgentStatus::Converged(outcome),
Err(e) => AgentStatus::Failed(e.to_string()),
}
}
pub(crate) fn data_dir_precondition(plugin: &Plugin) -> Result<()> {
crate::host::data_root(plugin).map(|_| ())
}
pub(crate) fn resolve(id: &str) -> Result<Box<dyn crate::agents::AgentBackend>> {
backend_for(id).ok_or_else(|| Error::Tree(format!("no backend for agent `{id}` (enable its cargo feature)")))
}