1use crate::{DocumentContext, DocumentStore, DocumentType, MetisError, Result, TemplateEngine};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub struct ProjectConfig {
10 pub name: String,
11 pub description: Option<String>,
12 pub root_path: PathBuf,
13}
14
15#[derive(Debug, Clone)]
17pub struct ProjectMetadata {
18 pub project_path: PathBuf,
19 pub database_path: PathBuf,
20}
21
22pub async fn initialize_project(config: ProjectConfig) -> Result<ProjectMetadata> {
24 let project_path = config.root_path.join("metis");
25 let database_path = project_path.join(".metis.db");
26
27 if !is_valid_project_name(&config.name) {
29 return Err(MetisError::ValidationFailed {
30 message: format!("Invalid project name '{}'. Use only alphanumeric characters, hyphens, and underscores.", config.name),
31 });
32 }
33
34 if !config.root_path.exists() {
36 return Err(MetisError::ValidationFailed {
37 message: format!(
38 "Parent directory does not exist: {}",
39 config.root_path.display()
40 ),
41 });
42 }
43
44 if !config.root_path.is_dir() {
45 return Err(MetisError::ValidationFailed {
46 message: format!("Path is not a directory: {}", config.root_path.display()),
47 });
48 }
49
50 if !project_path.exists() {
52 fs::create_dir_all(&project_path).map_err(|e| MetisError::ValidationFailed {
53 message: format!("Failed to create metis directory: {}", e),
54 })?;
55 }
56
57 let temp_file = project_path.join(".metis_temp_test");
59 if let Err(e) = fs::write(&temp_file, "") {
60 return Err(MetisError::ValidationFailed {
61 message: format!("Directory is not writable: {}", e),
62 });
63 }
64 let _ = fs::remove_file(temp_file); create_directory_structure(&project_path)?;
68
69 if !database_path.exists() {
71 let database_url = format!("sqlite:{}", database_path.display());
72 let _store = DocumentStore::new(&database_url).await?;
73 }
74
75 create_initial_vision(&project_path, &config.name, config.description.as_deref())?;
77
78 Ok(ProjectMetadata {
79 project_path,
80 database_path,
81 })
82}
83
84fn create_directory_structure(project_path: &Path) -> Result<()> {
86 let strategies_dir = project_path.join("strategies");
87 let decisions_dir = project_path.join("decisions");
88
89 fs::create_dir_all(&strategies_dir).map_err(|e| MetisError::ValidationFailed {
91 message: format!("Failed to create strategies directory: {}", e),
92 })?;
93
94 fs::create_dir_all(&decisions_dir).map_err(|e| MetisError::ValidationFailed {
95 message: format!("Failed to create decisions directory: {}", e),
96 })?;
97
98 Ok(())
99}
100
101fn create_initial_vision(
103 project_path: &Path,
104 project_name: &str,
105 description: Option<&str>,
106) -> Result<()> {
107 let vision_path = project_path.join("vision.md");
108
109 if vision_path.exists() {
111 return Ok(());
112 }
113
114 let template_engine = TemplateEngine::new()?;
115
116 let vision_context = DocumentContext::new(format!("{} Vision", project_name));
118
119 let vision_content = template_engine.render_document(&DocumentType::Vision, &vision_context)?;
121
122 let final_content = if let Some(desc) = description {
124 vision_content.replace("{Why this vision exists and what it aims to achieve}", desc)
126 } else {
127 vision_content
128 };
129
130 fs::write(&vision_path, final_content).map_err(|e| MetisError::ValidationFailed {
132 message: format!("Failed to create vision document: {}", e),
133 })?;
134
135 Ok(())
136}
137
138fn is_valid_project_name(name: &str) -> bool {
140 if name.is_empty() || name.len() > 255 {
141 return false;
142 }
143
144 name.chars()
146 .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' ')
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::path::PathBuf;
153 use tempfile::TempDir;
154
155 #[test]
156 fn test_is_valid_project_name() {
157 assert!(is_valid_project_name("my-project"));
158 assert!(is_valid_project_name("my_project"));
159 assert!(is_valid_project_name("MyProject123"));
160 assert!(is_valid_project_name("My Project"));
161
162 assert!(!is_valid_project_name(""));
163 assert!(!is_valid_project_name("my/project"));
164 assert!(!is_valid_project_name("my\\project"));
165 assert!(!is_valid_project_name("my<project>"));
166 assert!(!is_valid_project_name("my|project"));
167
168 let long_name = "a".repeat(256);
170 assert!(!is_valid_project_name(&long_name));
171 }
172
173 #[tokio::test]
174 async fn test_initialize_project_success() {
175 let temp_dir = TempDir::new().unwrap();
176 let project_path = temp_dir.path().to_path_buf();
177
178 let config = ProjectConfig {
179 name: "Test Project".to_string(),
180 description: Some("A test project for validation".to_string()),
181 root_path: project_path.clone(),
182 };
183
184 let result = initialize_project(config).await;
185 assert!(result.is_ok());
186
187 let metadata = result.unwrap();
188 assert_eq!(metadata.project_path, project_path.join("metis"));
189 assert_eq!(
190 metadata.database_path,
191 project_path.join("metis").join(".metis.db")
192 );
193
194 assert!(project_path.join("metis").join("strategies").exists());
196 assert!(project_path.join("metis").join("decisions").exists());
197
198 assert!(project_path.join("metis").join(".metis.db").exists());
200
201 let vision_path = project_path.join("metis").join("vision.md");
203 assert!(vision_path.exists());
204
205 let vision_content = fs::read_to_string(vision_path).unwrap();
206 assert!(vision_content.contains("Test Project Vision"));
207 assert!(vision_content.contains("A test project for validation"));
208 }
209
210 #[tokio::test]
211 async fn test_initialize_project_already_exists() {
212 let temp_dir = TempDir::new().unwrap();
213 let project_path = temp_dir.path().to_path_buf();
214
215 fs::create_dir_all(project_path.join("metis")).unwrap();
217 fs::write(project_path.join("metis").join(".metis.db"), "").unwrap();
218
219 let config = ProjectConfig {
220 name: "Test Project".to_string(),
221 description: None,
222 root_path: project_path.clone(),
223 };
224
225 let result = initialize_project(config).await;
227 assert!(result.is_ok());
228
229 let metadata = result.unwrap();
230 assert_eq!(metadata.project_path, project_path.join("metis"));
231 assert_eq!(
232 metadata.database_path,
233 project_path.join("metis").join(".metis.db")
234 );
235 }
236
237 #[tokio::test]
238 async fn test_initialize_project_twice() {
239 let temp_dir = TempDir::new().unwrap();
240 let project_path = temp_dir.path().to_path_buf();
241
242 let config = ProjectConfig {
243 name: "Test Project".to_string(),
244 description: Some("A test project for double initialization".to_string()),
245 root_path: project_path.clone(),
246 };
247
248 let result1 = initialize_project(config.clone()).await;
250 assert!(result1.is_ok());
251
252 assert!(project_path.join("metis").join(".metis.db").exists());
254 assert!(project_path.join("metis").join("vision.md").exists());
255
256 let result2 = initialize_project(config).await;
258 assert!(result2.is_ok());
259
260 let metadata2 = result2.unwrap();
261 assert_eq!(metadata2.project_path, project_path.join("metis"));
262 assert_eq!(
263 metadata2.database_path,
264 project_path.join("metis").join(".metis.db")
265 );
266 }
267
268 #[tokio::test]
269 async fn test_initialize_project_invalid_name() {
270 let temp_dir = TempDir::new().unwrap();
271 let project_path = temp_dir.path().to_path_buf();
272
273 let config = ProjectConfig {
274 name: "invalid/name".to_string(), description: None,
276 root_path: project_path.clone(),
277 };
278
279 let result = initialize_project(config).await;
280 assert!(result.is_err());
281
282 if let Err(MetisError::ValidationFailed { message }) = result {
283 assert!(message.contains("Invalid project name"));
284 } else {
285 panic!("Expected ValidationFailed error");
286 }
287 }
288
289 #[tokio::test]
290 async fn test_initialize_project_nonexistent_directory() {
291 let nonexistent_path = PathBuf::from("/nonexistent/directory");
292
293 let config = ProjectConfig {
294 name: "Test Project".to_string(),
295 description: None,
296 root_path: nonexistent_path,
297 };
298
299 let result = initialize_project(config).await;
300 assert!(result.is_err());
301
302 if let Err(MetisError::ValidationFailed { message }) = result {
303 assert!(message.contains("Parent directory does not exist"));
304 } else {
305 panic!("Expected ValidationFailed error");
306 }
307 }
308
309 #[tokio::test]
310 async fn test_initialize_project_without_description() {
311 let temp_dir = TempDir::new().unwrap();
312 let project_path = temp_dir.path().to_path_buf();
313
314 let config = ProjectConfig {
315 name: "Simple Project".to_string(),
316 description: None,
317 root_path: project_path.clone(),
318 };
319
320 let result = initialize_project(config).await;
321 assert!(result.is_ok());
322
323 let vision_path = project_path.join("metis").join("vision.md");
325 assert!(vision_path.exists());
326
327 let vision_content = fs::read_to_string(vision_path).unwrap();
328 assert!(vision_content.contains("Simple Project Vision"));
329 assert!(vision_content.contains("{Why this vision exists and what it aims to achieve}"));
331 }
332
333 #[test]
334 fn test_create_directory_structure() {
335 let temp_dir = TempDir::new().unwrap();
336 let project_path = temp_dir.path();
337
338 let result = create_directory_structure(project_path);
339 assert!(result.is_ok());
340
341 assert!(project_path.join("strategies").exists());
342 assert!(project_path.join("decisions").exists());
343 assert!(project_path.join("strategies").is_dir());
344 assert!(project_path.join("decisions").is_dir());
345 }
346}