use std::borrow::Cow;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct InstallContext {
pub repo_root: PathBuf,
pub dry_run: bool,
pub force: bool,
pub schema_version: u32,
}
#[derive(Debug, Clone)]
pub struct InstallReport {
pub agent_id: String,
pub hook_path: PathBuf,
pub settings_path: PathBuf,
pub already_installed: bool,
pub paths_written: Vec<PathBuf>,
pub preview: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error("io error at {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(
"{path} exists but does not contain klasp's managed marker. \
Re-run with --force to overwrite, or remove the file manually."
)]
MarkerConflict { path: PathBuf },
#[error("could not parse {path} as JSON: {source}")]
SettingsParse {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("agent surface `{agent_id}` reports: {message}")]
Surface { agent_id: String, message: String },
}
#[derive(Debug, Clone)]
pub struct SurfaceWarning {
pub path: PathBuf,
pub message: Cow<'static, str>,
}
#[derive(Debug, Clone)]
pub enum DoctorFinding {
Ok(String),
Warn(String),
Fail(String),
Info(String),
}
pub trait AgentSurface: Send + Sync {
fn agent_id(&self) -> &'static str;
fn detect(&self, repo_root: &Path) -> bool;
fn install(&self, ctx: &InstallContext) -> Result<InstallReport, InstallError>;
fn uninstall(&self, repo_root: &Path, dry_run: bool) -> Result<Vec<PathBuf>, InstallError>;
fn render_hook_script(&self, ctx: &InstallContext) -> String;
fn hook_path(&self, repo_root: &Path) -> PathBuf;
fn settings_path(&self, repo_root: &Path) -> PathBuf;
fn install_with_warnings(
&self,
ctx: &InstallContext,
) -> Result<(InstallReport, Vec<SurfaceWarning>), InstallError> {
Ok((self.install(ctx)?, Vec::new()))
}
fn doctor_check(&self, repo_root: &Path, schema_version: u32) -> Vec<DoctorFinding> {
let agent_id = self.agent_id();
let hook_path = self.hook_path(repo_root);
let mut findings = Vec::new();
let actual = match std::fs::read_to_string(&hook_path) {
Ok(s) => s,
Err(_) => {
findings.push(DoctorFinding::Fail(format!(
"hook[{agent_id}]: {} not found; re-run `klasp install`",
hook_path.display()
)));
return findings;
}
};
let ctx = InstallContext {
repo_root: repo_root.to_path_buf(),
dry_run: false,
force: false,
schema_version,
};
let expected = self.render_hook_script(&ctx);
if actual == expected {
findings.push(DoctorFinding::Ok(format!(
"hook[{agent_id}]: current (schema v{schema_version})"
)));
} else {
findings.push(DoctorFinding::Fail(format!(
"hook[{agent_id}]: schema drift detected (re-run `klasp install`)"
)));
}
findings
}
}