1use std::path::Path;
2
3use anyhow::Result;
4
5use crate::index::{count_bean_formats, ArchiveIndex, Index};
6
7pub fn cmd_sync(beans_dir: &Path) -> Result<()> {
9 let (md_count, yaml_count) = count_bean_formats(beans_dir)?;
11
12 let index = Index::build(beans_dir)?;
13 let count = index.beans.len();
14 index.save(beans_dir)?;
15
16 let archive_index = ArchiveIndex::build(beans_dir)?;
18 let archive_count = archive_index.beans.len();
19 if archive_count > 0 || beans_dir.join("archive.yaml").exists() {
20 archive_index.save(beans_dir)?;
21 }
22
23 println!("Index rebuilt: {} beans indexed.", count);
24 if archive_count > 0 {
25 println!(
26 "Archive index rebuilt: {} archived beans indexed.",
27 archive_count
28 );
29 }
30
31 if md_count > 0 && yaml_count > 0 {
33 eprintln!();
34 eprintln!("Warning: Mixed bean formats detected!");
35 eprintln!(" {} .md files (current format)", md_count);
36 eprintln!(" {} .yaml files (legacy format)", yaml_count);
37 eprintln!();
38 eprintln!("This can cause confusion. Consider migrating legacy files:");
39 eprintln!(" - Remove or archive .yaml files: mkdir -p .beans/legacy && mv .beans/*.yaml .beans/legacy/");
40 eprintln!(" - Or run 'bn doctor' for more details");
41 }
42
43 Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::bean::Bean;
50 use crate::util::title_to_slug;
51 use std::fs;
52 use tempfile::TempDir;
53
54 #[test]
55 fn sync_rebuilds_index() {
56 let dir = TempDir::new().unwrap();
57 let beans_dir = dir.path().join(".beans");
58 fs::create_dir(&beans_dir).unwrap();
59
60 let bean1 = Bean::new("1", "Task one");
61 let bean2 = Bean::new("2", "Task two");
62
63 let slug1 = title_to_slug(&bean1.title);
64 let slug2 = title_to_slug(&bean2.title);
65
66 bean1
67 .to_file(beans_dir.join(format!("1-{}.md", slug1)))
68 .unwrap();
69 bean2
70 .to_file(beans_dir.join(format!("2-{}.md", slug2)))
71 .unwrap();
72
73 let result = cmd_sync(&beans_dir);
75 assert!(result.is_ok());
76
77 assert!(beans_dir.join("index.yaml").exists());
79
80 let index = Index::load(&beans_dir).unwrap();
82 assert_eq!(index.beans.len(), 2);
83 }
84
85 #[test]
86 fn sync_counts_beans() {
87 let dir = TempDir::new().unwrap();
88 let beans_dir = dir.path().join(".beans");
89 fs::create_dir(&beans_dir).unwrap();
90
91 for i in 1..=5 {
93 let bean = Bean::new(i.to_string(), format!("Task {}", i));
94 let slug = title_to_slug(&bean.title);
95 bean.to_file(beans_dir.join(format!("{}-{}.md", i, slug)))
96 .unwrap();
97 }
98
99 let result = cmd_sync(&beans_dir);
100 assert!(result.is_ok());
101
102 let index = Index::load(&beans_dir).unwrap();
103 assert_eq!(index.beans.len(), 5);
104 }
105
106 #[test]
107 fn sync_empty_project() {
108 let dir = TempDir::new().unwrap();
109 let beans_dir = dir.path().join(".beans");
110 fs::create_dir(&beans_dir).unwrap();
111
112 let result = cmd_sync(&beans_dir);
113 assert!(result.is_ok());
114
115 let index = Index::load(&beans_dir).unwrap();
116 assert_eq!(index.beans.len(), 0);
117 }
118
119 #[test]
120 fn sync_rebuilds_archive_yaml() {
121 let dir = TempDir::new().unwrap();
122 let beans_dir = dir.path().join(".beans");
123 fs::create_dir(&beans_dir).unwrap();
124
125 let archive_dir = beans_dir.join("archive").join("2026").join("03");
127 fs::create_dir_all(&archive_dir).unwrap();
128
129 let mut bean1 = Bean::new("10", "Archived ten");
130 bean1.status = crate::bean::Status::Closed;
131 bean1.is_archived = true;
132 let slug1 = title_to_slug(&bean1.title);
133 bean1
134 .to_file(archive_dir.join(format!("10-{}.md", slug1)))
135 .unwrap();
136
137 let mut bean2 = Bean::new("20", "Archived twenty");
138 bean2.status = crate::bean::Status::Closed;
139 bean2.is_archived = true;
140 let slug2 = title_to_slug(&bean2.title);
141 bean2
142 .to_file(archive_dir.join(format!("20-{}.md", slug2)))
143 .unwrap();
144
145 cmd_sync(&beans_dir).unwrap();
147
148 assert!(beans_dir.join("archive.yaml").exists());
149 let archive = ArchiveIndex::load(&beans_dir).unwrap();
150 assert_eq!(archive.beans.len(), 2);
151 let ids: Vec<&str> = archive.beans.iter().map(|e| e.id.as_str()).collect();
152 assert!(ids.contains(&"10"));
153 assert!(ids.contains(&"20"));
154 }
155
156 #[test]
157 fn sync_does_not_create_archive_yaml_when_no_archive() {
158 let dir = TempDir::new().unwrap();
159 let beans_dir = dir.path().join(".beans");
160 fs::create_dir(&beans_dir).unwrap();
161
162 cmd_sync(&beans_dir).unwrap();
163
164 assert!(!beans_dir.join("archive.yaml").exists());
166 }
167}