use std::path::PathBuf;
use super::mcpjson::{self, ServerShape};
use super::report;
use super::skillsdir;
use super::{AgentBackend, BackendState};
use crate::doctor::{CheckStatus, DoctorCheck, DoctorReport};
use crate::error::{Error, Result};
use crate::host::{Capabilities, Desired, Outcome, Plugin, Scope, Source};
pub(crate) struct KiroBackend;
impl AgentBackend for KiroBackend {
fn id(&self) -> &'static str {
"kiro"
}
fn detect(&self) -> bool {
which::which("kiro-cli").is_ok() || user_base().is_ok_and(|b| b.is_dir())
}
fn capabilities(&self) -> Capabilities {
Capabilities {
plugins: false,
mcp: true,
hooks: false,
commands: false,
agents: false,
skills: true,
instructions: false,
scopes: &["user", "project"],
}
}
fn probe(&self, plugin: &Plugin, scope: &Scope, source: &Source) -> Result<BackendState> {
let comp = plugin.components(source)?.with_client(self.id());
let mcp = mcpjson::probe(&mcp_path(scope)?, &["mcpServers"], &comp.mcp_servers, ServerShape::plain())?;
let skills = skillsdir::probe(&kiro_base(scope)?.join("skills"), plugin, &comp.skills)?;
Ok(report::compose([Some(mcp), skills].into_iter().flatten()))
}
fn reconcile(&self, plugin: &Plugin, desired: &Desired, scope: &Scope) -> Result<Outcome> {
let comp = plugin.components(&desired.source)?.with_client(self.id());
let mut changed = mcpjson::reconcile(&mcp_path(scope)?, &["mcpServers"], &comp.mcp_servers, ServerShape::plain())? != Outcome::NoOp;
changed |= skillsdir::reconcile(&kiro_base(scope)?.join("skills"), plugin, &comp.skills)?;
Ok(if changed { Outcome::Installed } else { Outcome::NoOp })
}
fn remove(&self, plugin: &Plugin, scope: &Scope, source: &Source) -> Result<Outcome> {
let comp = plugin.components(source)?.with_client(self.id());
let mut changed = mcpjson::remove(&mcp_path(scope)?, &["mcpServers"], &comp.mcp_servers, ServerShape::plain())? != Outcome::NoOp;
changed |= skillsdir::remove(&kiro_base(scope)?.join("skills"), plugin, &comp.skills)?;
Ok(if changed { Outcome::Removed } else { Outcome::NoOp })
}
fn report(&self, plugin: &Plugin, source: &Source) -> DoctorReport {
DoctorReport::from_checks(report_checks(self, plugin, source))
}
}
fn user_base() -> Result<PathBuf> {
if let Some(dir) = std::env::var_os("KIRO_HOME").filter(|v| !v.is_empty()) {
return Ok(PathBuf::from(dir));
}
dirs::home_dir().map(|h| h.join(".kiro")).ok_or_else(|| Error::Tree("no home directory (HOME unset); cannot locate ~/.kiro".into()))
}
fn kiro_base(scope: &Scope) -> Result<PathBuf> {
match scope {
Scope::User => user_base(),
Scope::Project { path } => Ok(path.join(".kiro")),
}
}
fn mcp_path(scope: &Scope) -> Result<PathBuf> {
Ok(kiro_base(scope)?.join("settings").join("mcp.json"))
}
fn report_checks(backend: &KiroBackend, plugin: &Plugin, source: &Source) -> Vec<DoctorCheck> {
let mut checks = Vec::new();
checks.push(if backend.detect() {
DoctorCheck { name: "kiro detected", status: CheckStatus::Ok("`kiro-cli` on PATH or ~/.kiro present".into()) }
} else {
DoctorCheck {
name: "kiro detected",
status: CheckStatus::Fail {
problem: "kiro CLI not detected".into(),
fix: "install it with `curl -fsSL https://cli.kiro.dev/install | bash`".into(),
},
}
});
let mcp = match mcp_path(&Scope::User) {
Ok(path) => path,
Err(e) => {
checks.push(DoctorCheck { name: "mcp config file", status: CheckStatus::Warn(e.to_string()) });
return checks;
}
};
let root = report::read_json_config(&mut checks, "mcp config file", &mcp);
let Some(comp) = report::components(&mut checks, plugin, source).map(|c| c.with_client(backend.id())) else {
return checks;
};
checks.push(report::check_mcp_registered(
&comp.mcp_servers,
root.as_ref(),
&["mcpServers"],
"not in mcp.json",
"run the host's `setup`",
));
checks.push(report::check_mcp_command(&comp.mcp_servers));
checks
}
#[cfg(test)]
#[path = "../../tests/unit/kiro.rs"]
mod kiro_tests;