use std::path::{Path, PathBuf};
use trusty_mpm::core::agent_manifest::{
AgentManifest, MANIFEST_FILE, ManifestLoad, repair_stale_tmp,
};
use trusty_mpm::core::skill_manifest::{SKILL_MANIFEST_FILE, SkillManifest};
pub(crate) fn repair_deploy(force: bool) -> anyhow::Result<()> {
let home =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve home directory"))?;
let claude_dir = home.join(".claude");
let agents_dir = claude_dir.join("agents");
let skills_dir = claude_dir.join("skills");
let agents_tmps = remove_tmp_orphans(&agents_dir)?;
if agents_tmps.is_empty() {
println!("agents: no stale .tmp files found");
} else {
for p in &agents_tmps {
println!("agents: removed stale temp file {}", p.display());
}
}
let agent_manifest_path = agents_dir.join(MANIFEST_FILE);
if agent_manifest_path.exists() {
match AgentManifest::load_checked(&agents_dir) {
ManifestLoad::Ok(_) => {
println!("agents: manifest OK");
}
ManifestLoad::Corrupt(detail) => {
eprintln!("agents: manifest CORRUPT — {detail}");
if force {
let empty = AgentManifest::default();
empty
.save(&agents_dir)
.map_err(|e| anyhow::anyhow!("failed to reset agent manifest: {e}"))?;
println!(
"agents: manifest reset to empty (--force). Run `tm install` to re-deploy."
);
} else {
println!(
"agents: run `tm repair deploy --force` to reset and re-deploy, \
or `tm install --force` to force a full re-install."
);
}
}
}
} else {
println!("agents: no manifest found (first deploy not yet run)");
}
let skill_tmps = remove_skill_tmp_orphans(&skills_dir)?;
if skill_tmps.is_empty() {
println!("skills: no stale .tmp files found");
} else {
for p in &skill_tmps {
println!("skills: removed stale temp file {}", p.display());
}
}
let skill_manifest_path = skills_dir.join(SKILL_MANIFEST_FILE);
if skill_manifest_path.exists() {
match validate_skill_manifest(&skills_dir) {
Ok(()) => println!("skills: manifest OK"),
Err(detail) => {
eprintln!("skills: manifest CORRUPT — {detail}");
if force {
let empty = SkillManifest::default();
empty
.save(&skills_dir)
.map_err(|e| anyhow::anyhow!("failed to reset skill manifest: {e}"))?;
println!(
"skills: manifest reset to empty (--force). Run `tm install` to re-deploy."
);
} else {
println!(
"skills: run `tm repair deploy --force` to reset and re-deploy, \
or `tm install --force` to force a full re-install."
);
}
}
}
} else {
println!("skills: no manifest found (first deploy not yet run)");
}
Ok(())
}
fn remove_tmp_orphans(dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
let mut removed = Vec::new();
if !dir.is_dir() {
return Ok(removed);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.ends_with(".tmp") && entry.file_type()?.is_file() {
let path = entry.path();
let base = path.with_extension("");
repair_stale_tmp(&base)?;
removed.push(path);
}
}
Ok(removed)
}
fn remove_skill_tmp_orphans(skills_dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
let mut all_removed = Vec::new();
if !skills_dir.is_dir() {
return Ok(all_removed);
}
let mut top = remove_tmp_orphans(skills_dir)?;
all_removed.append(&mut top);
for entry in std::fs::read_dir(skills_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let mut sub = remove_tmp_orphans(&entry.path())?;
all_removed.append(&mut sub);
}
}
Ok(all_removed)
}
fn validate_skill_manifest(skills_dir: &Path) -> Result<(), String> {
let path = skills_dir.join(SKILL_MANIFEST_FILE);
match std::fs::read_to_string(&path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(format!("{}: {e}", path.display())),
Ok(raw) => serde_json::from_str::<SkillManifest>(&raw)
.map(|_| ())
.map_err(|e| format!("{}: {e}", path.display())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use trusty_mpm::core::agent_manifest::MANIFEST_FILE;
fn setup_agents_dir_with_stale_tmp(agents_dir: &Path) {
fs::create_dir_all(agents_dir).unwrap();
fs::write(agents_dir.join("engineer.tmp"), "incomplete content").unwrap();
}
#[test]
fn remove_tmp_orphans_removes_only_tmp_files() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("engineer.md"), "good content").unwrap();
fs::write(tmp.path().join("engineer.tmp"), "stale").unwrap();
fs::write(tmp.path().join("other.tmp"), "stale2").unwrap();
let removed = remove_tmp_orphans(tmp.path()).unwrap();
assert_eq!(removed.len(), 2, "expected 2 .tmp files removed");
assert!(
tmp.path().join("engineer.md").exists(),
".md file must survive"
);
assert!(
!tmp.path().join("engineer.tmp").exists(),
".tmp file must be removed"
);
}
#[test]
fn remove_tmp_orphans_is_no_op_on_missing_dir() {
let removed = remove_tmp_orphans(Path::new("/nonexistent/dir/agents")).unwrap();
assert!(removed.is_empty());
}
#[test]
fn validate_skill_manifest_ok_on_missing_file() {
let tmp = TempDir::new().unwrap();
assert!(validate_skill_manifest(tmp.path()).is_ok());
}
#[test]
fn validate_skill_manifest_err_on_corrupt_file() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(SKILL_MANIFEST_FILE), b"not valid json{{{").unwrap();
assert!(validate_skill_manifest(tmp.path()).is_err());
}
#[test]
fn repair_deploy_removes_stale_tmps() {
let agents_dir = TempDir::new().unwrap();
setup_agents_dir_with_stale_tmp(agents_dir.path());
assert!(agents_dir.path().join("engineer.tmp").exists());
let removed = remove_tmp_orphans(agents_dir.path()).unwrap();
assert_eq!(removed.len(), 1);
assert!(!agents_dir.path().join("engineer.tmp").exists());
}
#[test]
fn repair_deploy_corrupt_manifest_is_reported_without_force() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(MANIFEST_FILE), b"garbage{{{").unwrap();
let result = AgentManifest::load_checked(tmp.path());
assert!(
matches!(result, ManifestLoad::Corrupt(_)),
"corrupt manifest must be detected"
);
assert!(tmp.path().join(MANIFEST_FILE).exists());
}
}