use crate::agents::{AgentBackend, BackendState};
use crate::error::Result;
use crate::host::{AgentReport, AgentStatus, Desired, Outcome, Plugin, Scope, SkipReason, Source};
use crate::install::resolve;
use crate::{lock, restart, stamp};
pub(crate) fn self_heal_report(plugin: &Plugin, source: Source) -> Result<AgentReport> {
let scope = Scope::User;
let _lock = lock::acquire()?;
crate::install::data_dir_precondition(plugin)?;
let mut report = AgentReport::new();
for id in plugin.agents {
report.push(id, heal_status(plugin, &source, &scope, id));
}
Ok(report)
}
fn heal_status(plugin: &Plugin, source: &Source, scope: &Scope, id: &'static str) -> 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 marker = match stamp::read(plugin, scope, id) {
Ok(marker) => marker,
Err(e) => return AgentStatus::Failed(e.to_string()),
};
let resolved_source = stamp::source_from_marker(marker.as_ref(), source.clone());
if matches!(resolved_source, Source::GitHub { .. }) && !backend.capabilities().plugins {
return AgentStatus::Skipped(SkipReason::SourceUnsupported);
}
let interrupted = marker.as_ref().is_some_and(|m| m.reinstalling);
match heal_agent(&*backend, plugin, resolved_source, scope, marker.is_some(), interrupted, id == "claude") {
Ok(outcome) => AgentStatus::Converged(outcome),
Err(e) => AgentStatus::Failed(e.to_string()),
}
}
fn heal_agent(
backend: &dyn AgentBackend, plugin: &Plugin, resolved_source: Source, scope: &Scope, has_marker: bool, interrupted: bool,
is_claude: bool,
) -> Result<Outcome> {
let state = backend.probe(plugin, scope, &resolved_source)?;
let desired = Desired { source: resolved_source, reenable: false };
match (has_marker, state) {
(false, BackendState::Absent) => Ok(Outcome::NoOp),
(false, BackendState::Healthy) => {
if is_claude {
let _ = restart::clear(plugin);
}
let outcome = backend.reconcile(plugin, &desired, scope)?;
stamp::write(plugin, scope, &desired.source, backend.id())?;
Ok(match outcome {
Outcome::NoOp => Outcome::Adopted,
other => other,
})
}
(false, BackendState::Disabled | BackendState::NeedsRepair) => {
if is_claude {
let _ = restart::clear(plugin);
}
let outcome = backend.reconcile(plugin, &desired, scope)?;
stamp::write(plugin, scope, &desired.source, backend.id())?;
Ok(outcome)
}
(true, BackendState::Absent) if interrupted => {
let outcome = backend.reconcile(plugin, &desired, scope)?;
if is_claude && outcome != Outcome::NoOp {
let _ = restart::set(plugin);
}
stamp::write(plugin, scope, &desired.source, backend.id())?;
Ok(outcome)
}
(true, BackendState::Absent) => {
if is_claude {
let _ = restart::clear(plugin);
}
stamp::clear(plugin, scope, backend.id())?;
Ok(Outcome::Cleared)
}
(true, BackendState::Disabled) => Ok(Outcome::NoOp),
(true, BackendState::Healthy) => {
if is_claude {
let _ = restart::clear(plugin);
}
Ok(Outcome::NoOp)
}
(true, BackendState::NeedsRepair) => {
let outcome = backend.reconcile(plugin, &desired, scope)?;
if is_claude && outcome != Outcome::NoOp {
let _ = restart::set(plugin);
}
stamp::write(plugin, scope, &desired.source, backend.id())?;
Ok(outcome)
}
}
}