dot_agent_core/
profile.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use walkdir::WalkDir;
5
6use crate::error::{DotAgentError, Result};
7
8const PROFILES_DIR: &str = "profiles";
9const IGNORED_FILES: &[&str] = &[".git", ".DS_Store", ".gitignore", ".gitkeep"];
10const IGNORED_EXTENSIONS: &[&str] = &[];
11
12pub struct Profile {
13 pub name: String,
14 pub path: PathBuf,
15}
16
17impl Profile {
18 pub fn new(name: String, path: PathBuf) -> Self {
19 Self { name, path }
20 }
21
22 pub fn list_files(&self) -> Result<Vec<PathBuf>> {
24 let mut files = Vec::new();
25
26 for entry in WalkDir::new(&self.path).into_iter().filter_map(|e| e.ok()) {
27 let path = entry.path();
28 if path.is_file() && !should_ignore(path) {
29 if let Ok(relative) = path.strip_prefix(&self.path) {
30 files.push(relative.to_path_buf());
31 }
32 }
33 }
34
35 files.sort();
36 Ok(files)
37 }
38
39 pub fn contents_summary(&self) -> String {
41 let mut summary = Vec::new();
42
43 if let Ok(entries) = fs::read_dir(&self.path) {
44 for entry in entries.filter_map(|e| e.ok()) {
45 let path = entry.path();
46 if path.is_dir() {
47 let name = path.file_name().unwrap().to_string_lossy().to_string();
48 let count = WalkDir::new(&path)
49 .into_iter()
50 .filter_map(|e| e.ok())
51 .filter(|e| e.path().is_file() && !should_ignore(e.path()))
52 .count();
53 if count > 0 {
54 summary.push(format!("{} ({})", name, count));
55 }
56 } else if path.is_file() {
57 let name = path.file_name().unwrap().to_string_lossy().to_string();
58 if !should_ignore(&path) {
59 summary.push(name);
60 }
61 }
62 }
63 }
64
65 if summary.is_empty() {
66 "(empty)".to_string()
67 } else {
68 summary.join(", ")
69 }
70 }
71}
72
73pub struct ProfileManager {
74 base_dir: PathBuf,
75}
76
77impl ProfileManager {
78 pub fn new(base_dir: PathBuf) -> Self {
79 Self { base_dir }
80 }
81
82 pub fn profiles_dir(&self) -> PathBuf {
83 self.base_dir.join(PROFILES_DIR)
84 }
85
86 pub fn list_profiles(&self) -> Result<Vec<Profile>> {
88 let profiles_dir = self.profiles_dir();
89 if !profiles_dir.exists() {
90 return Ok(Vec::new());
91 }
92
93 let mut profiles = Vec::new();
94 for entry in fs::read_dir(&profiles_dir)? {
95 let entry = entry?;
96 let path = entry.path();
97 if path.is_dir() {
98 let name = path.file_name().unwrap().to_string_lossy().to_string();
99 profiles.push(Profile::new(name, path));
100 }
101 }
102
103 profiles.sort_by(|a, b| a.name.cmp(&b.name));
104 Ok(profiles)
105 }
106
107 pub fn get_profile(&self, name: &str) -> Result<Profile> {
109 let path = self.profiles_dir().join(name);
110 if !path.exists() {
111 return Err(DotAgentError::ProfileNotFound {
112 name: name.to_string(),
113 });
114 }
115 Ok(Profile::new(name.to_string(), path))
116 }
117
118 pub fn create_profile(&self, name: &str) -> Result<Profile> {
120 validate_profile_name(name)?;
121
122 let path = self.profiles_dir().join(name);
123 if path.exists() {
124 return Err(DotAgentError::ProfileAlreadyExists {
125 name: name.to_string(),
126 });
127 }
128
129 fs::create_dir_all(&path)?;
130 Ok(Profile::new(name.to_string(), path))
131 }
132
133 pub fn remove_profile(&self, name: &str) -> Result<()> {
135 let profile = self.get_profile(name)?;
136 fs::remove_dir_all(&profile.path)?;
137 Ok(())
138 }
139
140 pub fn import_profile(&self, source: &Path, name: &str, force: bool) -> Result<Profile> {
142 validate_profile_name(name)?;
143
144 if !source.exists() {
145 return Err(DotAgentError::TargetNotFound {
146 path: source.to_path_buf(),
147 });
148 }
149
150 let dest = self.profiles_dir().join(name);
151
152 if dest.exists() {
153 if !force {
154 return Err(DotAgentError::ProfileAlreadyExists {
155 name: name.to_string(),
156 });
157 }
158 fs::remove_dir_all(&dest)?;
159 }
160
161 fs::create_dir_all(self.profiles_dir())?;
163
164 copy_dir_recursive(source, &dest)?;
166
167 Ok(Profile::new(name.to_string(), dest))
168 }
169}
170
171fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
172 fs::create_dir_all(dst)?;
173
174 for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
175 let src_path = entry.path();
176 let relative = src_path.strip_prefix(src).unwrap();
177 let dst_path = dst.join(relative);
178
179 if src_path.is_dir() {
180 fs::create_dir_all(&dst_path)?;
181 } else if src_path.is_file() && !should_ignore(src_path) {
182 if let Some(parent) = dst_path.parent() {
183 fs::create_dir_all(parent)?;
184 }
185 fs::copy(src_path, &dst_path)?;
186 }
187 }
188
189 Ok(())
190}
191
192fn validate_profile_name(name: &str) -> Result<()> {
193 if name.is_empty() || name.len() > 64 {
194 return Err(DotAgentError::InvalidProfileName {
195 name: name.to_string(),
196 });
197 }
198
199 let first_char = name.chars().next().unwrap();
200 if !first_char.is_ascii_alphabetic() {
201 return Err(DotAgentError::InvalidProfileName {
202 name: name.to_string(),
203 });
204 }
205
206 for c in name.chars() {
207 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
208 return Err(DotAgentError::InvalidProfileName {
209 name: name.to_string(),
210 });
211 }
212 }
213
214 Ok(())
215}
216
217fn should_ignore(path: &Path) -> bool {
218 if let Some(name) = path.file_name() {
219 let name = name.to_string_lossy();
220 if IGNORED_FILES.contains(&name.as_ref()) {
221 return true;
222 }
223 }
224
225 if let Some(ext) = path.extension() {
226 let ext = ext.to_string_lossy();
227 if IGNORED_EXTENSIONS.contains(&ext.as_ref()) {
228 return true;
229 }
230 }
231
232 false
233}