1use std::fs;
2use std::path::{Path, PathBuf};
3use tracing::{debug, info, warn};
4
5use crate::error::{ModeError, ModeResult};
6use crate::mode::MiyabiMode;
7
8pub struct ModeLoader {
10 modes_dir: PathBuf,
11}
12
13impl ModeLoader {
14 pub fn new(project_root: &Path) -> Self {
16 Self {
17 modes_dir: project_root.join(".miyabi/modes"),
18 }
19 }
20
21 pub fn load_all(&self) -> ModeResult<Vec<MiyabiMode>> {
23 let mut modes = Vec::new();
24
25 match self.load_system_modes() {
27 Ok(system_modes) => {
28 info!("Loaded {} system modes", system_modes.len());
29 modes.extend(system_modes);
30 },
31 Err(e) => {
32 warn!("Failed to load system modes: {}", e);
33 },
34 }
35
36 match self.load_custom_modes() {
38 Ok(custom_modes) => {
39 info!("Loaded {} custom modes", custom_modes.len());
40 modes.extend(custom_modes);
41 },
42 Err(e) => {
43 debug!("No custom modes loaded: {}", e);
44 },
45 }
46
47 if modes.is_empty() {
48 return Err(ModeError::InvalidDefinition(
49 "No modes loaded. Check .miyabi/modes directory.".into(),
50 ));
51 }
52
53 Ok(modes)
54 }
55
56 fn load_system_modes(&self) -> ModeResult<Vec<MiyabiMode>> {
58 let system_dir = self.modes_dir.join("system");
59 if !system_dir.exists() {
60 return Err(ModeError::InvalidDefinition(format!(
61 "System modes directory not found: {}",
62 system_dir.display()
63 )));
64 }
65 self.load_from_dir(&system_dir)
66 }
67
68 fn load_custom_modes(&self) -> ModeResult<Vec<MiyabiMode>> {
70 let custom_dir = self.modes_dir.join("custom");
71 if !custom_dir.exists() {
72 return Ok(Vec::new());
73 }
74 self.load_from_dir(&custom_dir)
75 }
76
77 fn load_from_dir(&self, dir: &Path) -> ModeResult<Vec<MiyabiMode>> {
79 let mut modes = Vec::new();
80
81 for entry in fs::read_dir(dir)? {
82 let entry = entry?;
83 let path = entry.path();
84
85 if path.extension().and_then(|s| s.to_str()) == Some("yaml") {
86 match self.load_file(&path) {
87 Ok(mode) => {
88 debug!("Loaded mode '{}' from {:?}", mode.slug, path);
89 modes.push(mode);
90 },
91 Err(e) => {
92 warn!("Failed to load mode from {:?}: {}", path, e);
93 },
94 }
95 }
96 }
97
98 Ok(modes)
99 }
100
101 fn load_file(&self, path: &Path) -> ModeResult<MiyabiMode> {
103 let yaml = fs::read_to_string(path)?;
104 let mode: MiyabiMode = serde_yaml::from_str(&yaml)?;
105
106 if mode.slug.is_empty() {
108 return Err(ModeError::MissingField("slug".into()));
109 }
110 if mode.name.is_empty() {
111 return Err(ModeError::MissingField("name".into()));
112 }
113
114 if let Some(ref regex) = mode.file_regex {
116 regex::Regex::new(regex)?;
117 }
118
119 Ok(mode)
120 }
121
122 pub fn modes_dir(&self) -> &Path {
124 &self.modes_dir
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use tempfile::TempDir;
132
133 fn create_test_mode_yaml(dir: &Path, slug: &str) -> std::io::Result<()> {
134 let yaml = format!(
135 r#"
136slug: {}
137name: "Test Mode"
138character: "てすとん"
139roleDefinition: "Test role"
140whenToUse: "Test usage"
141groups:
142 - read
143 - edit
144customInstructions: "Test instructions"
145source: "user"
146"#,
147 slug
148 );
149 fs::write(dir.join(format!("{}.yaml", slug)), yaml)
150 }
151
152 #[test]
153 fn test_load_from_dir() {
154 let temp_dir = TempDir::new().unwrap();
155 let modes_dir = temp_dir.path().join(".miyabi/modes/custom");
156 fs::create_dir_all(&modes_dir).unwrap();
157
158 create_test_mode_yaml(&modes_dir, "test1").unwrap();
159 create_test_mode_yaml(&modes_dir, "test2").unwrap();
160
161 let loader = ModeLoader::new(temp_dir.path());
162 let modes = loader.load_from_dir(&modes_dir).unwrap();
163
164 assert_eq!(modes.len(), 2);
165 assert!(modes.iter().any(|m| m.slug == "test1"));
166 assert!(modes.iter().any(|m| m.slug == "test2"));
167 }
168
169 #[test]
170 fn test_missing_slug() {
171 let temp_dir = TempDir::new().unwrap();
172 let modes_dir = temp_dir.path().join(".miyabi/modes/custom");
173 fs::create_dir_all(&modes_dir).unwrap();
174
175 let invalid_yaml = r#"
176name: "Test Mode"
177character: "てすとん"
178roleDefinition: "Test"
179whenToUse: "Test"
180groups: [read]
181customInstructions: "Test"
182source: "user"
183"#;
184 fs::write(modes_dir.join("invalid.yaml"), invalid_yaml).unwrap();
185
186 let loader = ModeLoader::new(temp_dir.path());
187 let result = loader.load_file(&modes_dir.join("invalid.yaml"));
188
189 assert!(result.is_err());
190 }
191}