1use std::path::Path;
2
3pub fn upsert(path: &Path, start: &str, end: &str, block: &str, quiet: bool, label: &str) {
4 let existing = std::fs::read_to_string(path).unwrap_or_default();
5
6 if existing.contains(start) {
7 let cleaned = remove_content(&existing, start, end);
8 let mut out = cleaned.trim_end().to_string();
9 if !out.is_empty() {
10 out.push('\n');
11 }
12 out.push('\n');
13 out.push_str(block);
14 out.push('\n');
15 std::fs::write(path, &out).ok();
16 if !quiet {
17 println!(" Updated {label}");
18 }
19 } else {
20 let mut out = existing;
21 if !out.is_empty() && !out.ends_with('\n') {
22 out.push('\n');
23 }
24 if !out.is_empty() {
25 out.push('\n');
26 }
27 out.push_str(block);
28 out.push('\n');
29 std::fs::write(path, &out).ok();
30 if !quiet {
31 println!(" Installed {label}");
32 }
33 }
34}
35
36pub fn remove_from_file(path: &Path, start: &str, end: &str, quiet: bool, label: &str) {
37 let Ok(existing) = std::fs::read_to_string(path) else {
38 return;
39 };
40 if !existing.contains(start) {
41 return;
42 }
43 let cleaned = remove_content(&existing, start, end);
44 std::fs::write(path, cleaned.trim_end().to_owned() + "\n").ok();
45 if !quiet {
46 println!(" Removed {label}");
47 }
48}
49
50pub fn remove_content(content: &str, start: &str, end: &str) -> String {
51 let s = content.find(start);
52 let e = content.find(end);
53 match (s, e) {
54 (Some(si), Some(ei)) if ei >= si => {
55 let after_end = ei + end.len();
56 let before = content[..si].trim_end_matches('\n');
57 let after = content[after_end..].trim_start_matches('\n');
58 let mut out = before.to_string();
59 if !after.is_empty() {
60 out.push('\n');
61 out.push_str(after);
62 }
63 out
64 }
65 _ => content.to_string(),
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn remove_content_works() {
75 let content = "before\n# >>> start >>>\nhook content\n# <<< end <<<\nafter\n";
76 let cleaned = remove_content(content, "# >>> start >>>", "# <<< end <<<");
77 assert!(!cleaned.contains("hook content"));
78 assert!(cleaned.contains("before"));
79 assert!(cleaned.contains("after"));
80 }
81
82 #[test]
83 fn remove_content_preserves_when_missing() {
84 let content = "no hook here\n";
85 let cleaned = remove_content(content, "# >>> start >>>", "# <<< end <<<");
86 assert_eq!(cleaned, content);
87 }
88}