1use std::fs;
2use std::path::Path;
3
4use anyhow::Context;
5use anyhow::Result;
6
7use crate::discovery::find_unit_file;
8use crate::index::Index;
9use crate::unit::Unit;
10
11pub struct DeleteResult {
13 pub id: String,
14 pub title: String,
15}
16
17pub fn delete(mana_dir: &Path, id: &str) -> Result<DeleteResult> {
19 let unit_path =
20 find_unit_file(mana_dir, id).with_context(|| format!("Unit not found: {}", id))?;
21 let unit =
22 Unit::from_file(&unit_path).with_context(|| format!("Failed to load unit: {}", id))?;
23 let title = unit.title.clone();
24 fs::remove_file(&unit_path).with_context(|| format!("Failed to delete: {}", id))?;
25 cleanup_dep_references(mana_dir, id)?;
26 let index = Index::build(mana_dir)?;
27 index.save(mana_dir)?;
28 Ok(DeleteResult {
29 id: id.to_string(),
30 title,
31 })
32}
33
34fn cleanup_dep_references(mana_dir: &Path, deleted_id: &str) -> Result<()> {
35 let dir_entries = fs::read_dir(mana_dir)
36 .with_context(|| format!("Failed to read: {}", mana_dir.display()))?;
37 for entry in dir_entries {
38 let entry = entry?;
39 let path = entry.path();
40 let filename = path
41 .file_name()
42 .and_then(|n| n.to_str())
43 .unwrap_or_default();
44 if filename == "index.yaml" || filename == "config.yaml" || filename == "unit.yaml" {
45 continue;
46 }
47 let ext = path.extension().and_then(|e| e.to_str());
48 let is_unit = match ext {
49 Some("md") => filename.contains('-'),
50 Some("yaml") => true,
51 _ => false,
52 };
53 if !is_unit {
54 continue;
55 }
56 if let Ok(mut unit) = Unit::from_file(&path) {
57 let n = unit.dependencies.len();
58 unit.dependencies.retain(|d| d != deleted_id);
59 if unit.dependencies.len() < n {
60 unit.to_file(&path)?;
61 }
62 }
63 }
64 Ok(())
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::ops::create::{self, tests::minimal_params};
71 use tempfile::TempDir;
72
73 fn setup() -> (TempDir, std::path::PathBuf) {
74 let dir = TempDir::new().unwrap();
75 let bd = dir.path().join(".mana");
76 fs::create_dir(&bd).unwrap();
77 crate::config::Config {
78 project: "test".into(),
79 next_id: 1,
80 auto_close_parent: true,
81 run: None,
82 plan: None,
83 max_loops: 10,
84 max_concurrent: 4,
85 poll_interval: 30,
86 extends: vec![],
87 rules_file: None,
88 file_locking: false,
89 worktree: false,
90 on_close: None,
91 on_fail: None,
92 post_plan: None,
93 verify_timeout: None,
94 review: None,
95 user: None,
96 user_email: None,
97 auto_commit: false,
98 commit_template: None,
99 research: None,
100 run_model: None,
101 plan_model: None,
102 review_model: None,
103 research_model: None,
104 batch_verify: false,
105 memory_reserve_mb: 0,
106 notify: None,
107 }
108 .save(&bd)
109 .unwrap();
110 (dir, bd)
111 }
112
113 #[test]
114 fn delete_unit() {
115 let (_dir, bd) = setup();
116 let c = create::create(&bd, minimal_params("Task")).unwrap();
117 assert!(c.path.exists());
118 let r = delete(&bd, "1").unwrap();
119 assert_eq!(r.title, "Task");
120 assert!(!c.path.exists());
121 }
122
123 #[test]
124 fn delete_nonexistent() {
125 let (_dir, bd) = setup();
126 assert!(delete(&bd, "99").is_err());
127 }
128
129 #[test]
130 fn delete_cleans_deps() {
131 let (_dir, bd) = setup();
132 create::create(&bd, minimal_params("A")).unwrap();
133 let mut p = minimal_params("B");
134 p.dependencies = vec!["1".into()];
135 create::create(&bd, p).unwrap();
136 delete(&bd, "1").unwrap();
137 let b2 = Unit::from_file(find_unit_file(&bd, "2").unwrap()).unwrap();
138 assert!(!b2.dependencies.contains(&"1".to_string()));
139 }
140
141 #[test]
142 fn delete_rebuilds_index() {
143 let (_dir, bd) = setup();
144 create::create(&bd, minimal_params("A")).unwrap();
145 create::create(&bd, minimal_params("B")).unwrap();
146 delete(&bd, "1").unwrap();
147 let index = Index::load(&bd).unwrap();
148 assert_eq!(index.units.len(), 1);
149 }
150}