//! Auto-detection of projects from current working directory //! //! This module provides intelligent project detection by: //! 1. Searching for .project/{config_filename} in CWD and parent directories //! 2. Reading project name from TOML configuration //! 3. Caching last used project for quick access //! //! # Examples //! ```no_run //! use tools_shared::detect_project_from_cwd; //! //! if let Some(project) = detect_project_from_cwd("config.toml") { //! println!("Detected project: {}", project.name); //! println!("Root path: {}", project.root_path.display()); //! } //! ``` use crate::xdg::{ensure_dir, tool_cache_dir}; use std::path::{Path, PathBuf}; /// Detected project information #[derive(Debug, Clone)] pub struct DetectedProject { /// Project name from config pub name: String, /// Path to configuration file (.project/lifecycle.toml) pub config_path: PathBuf, /// Root path of project (parent of .project/) pub root_path: PathBuf, } /// Auto-detect project from current working directory /// /// Searches for configuration file (.project/{config_filename}) in CWD and parent directories. /// /// # Arguments /// * `config_filename` - Configuration filename (e.g., "config.toml", "tracking.toml") /// /// # Returns /// Some(DetectedProject) if found, None otherwise /// /// # Examples /// ```no_run /// use tools_shared::detect_project_from_cwd; /// /// if let Some(project) = detect_project_from_cwd("config.toml") { /// println!("Project: {}", project.name); /// } /// ``` pub fn detect_project_from_cwd(config_filename: &str) -> Option { let cwd = std::env::current_dir().ok()?; detect_project_from_path(cwd, config_filename) } /// Auto-detect project from specific path /// /// Searches upward from given path for .project/{config_filename} /// /// # Arguments /// * `path` - Starting path to search from /// * `config_filename` - Configuration filename /// /// # Returns /// Some(DetectedProject) if found, None otherwise pub fn detect_project_from_path>( path: P, config_filename: &str, ) -> Option { let mut current = path.as_ref().to_path_buf(); loop { // Try .project/ let project_tools_config = current.join(".project").join(config_filename); if project_tools_config.exists() { if let Ok(project_name) = read_project_name(&project_tools_config) { return Some(DetectedProject { name: project_name, config_path: project_tools_config, root_path: current, }); } } // Try .vapora/ (VAPORA platform compatibility) let vapora_config = current.join(".vapora").join(config_filename); if vapora_config.exists() { if let Ok(project_name) = read_project_name(&vapora_config) { return Some(DetectedProject { name: project_name, config_path: vapora_config, root_path: current, }); } } // Move to parent directory if !current.pop() { break; } } None } /// Load last used project ID from cache /// /// # Arguments /// * `tool_name` - Name of the tool (e.g., "project-lifecycle") /// /// # Returns /// Some(project_id) if cached, None otherwise pub fn load_last_project(tool_name: &str) -> Option { let cache_file = tool_cache_dir(tool_name).join("last_project"); std::fs::read_to_string(cache_file).ok() } /// Save last used project ID to cache /// /// # Arguments /// * `tool_name` - Name of the tool /// * `project_id` - Project ID to cache /// /// # Errors /// Returns error if cache cannot be written pub fn save_last_project(tool_name: &str, project_id: &str) -> std::io::Result<()> { let cache_dir = tool_cache_dir(tool_name); ensure_dir(&cache_dir)?; let cache_file = cache_dir.join("last_project"); std::fs::write(cache_file, project_id) } /// Read project name from TOML configuration /// Expects: [project] section with name = "..." field fn read_project_name(config_path: &Path) -> Result> { let content = std::fs::read_to_string(config_path)?; // Simple TOML parsing for project.name field for line in content.lines() { let line = line.trim(); if line.starts_with("name") && line.contains('=') { // Extract value between quotes if let Some(start) = line.find('"') { if let Some(end) = line[start + 1..].find('"') { return Ok(line[start + 1..start + 1 + end].to_string()); } } } } Err("project.name not found in config".into()) } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_detect_project_from_path() { let temp = TempDir::new().expect("Failed to create temp dir"); let project_root = temp.path(); // Create .project/test.toml let config_dir = project_root.join(".project"); std::fs::create_dir(&config_dir).expect("Failed to create dir"); let config_content = r#" [project] name = "test-project" description = "Test project" "#; let config_file = config_dir.join("test.toml"); std::fs::write(&config_file, config_content).expect("Failed to write config"); // Test detection let detected = detect_project_from_path(project_root, "test.toml").expect("Project not detected"); assert_eq!(detected.name, "test-project"); assert_eq!(detected.config_path, config_file); assert_eq!(detected.root_path, project_root); } #[test] fn test_detect_project_with_vapora() { let temp = TempDir::new().expect("Failed to create temp dir"); let project_root = temp.path(); // Create .vapora/test.toml let config_dir = project_root.join(".vapora"); std::fs::create_dir(&config_dir).expect("Failed to create dir"); let config_content = r#" [project] name = "vapora-project" "#; let config_file = config_dir.join("test.toml"); std::fs::write(&config_file, config_content).expect("Failed to write config"); // Test detection let detected = detect_project_from_path(project_root, "test.toml").expect("Project not detected"); assert_eq!(detected.name, "vapora-project"); } #[test] fn test_detect_project_not_found() { let temp = TempDir::new().expect("Failed to create temp dir"); let result = detect_project_from_path(temp.path(), "nonexistent.toml"); assert!(result.is_none()); } #[test] fn test_save_and_load_last_project() { use crate::xdg::tool_cache_dir; let tool_name = "test-tool-xyz"; let project_id = "my-test-project"; // Clean up any existing cache let _ = std::fs::remove_dir_all(tool_cache_dir(tool_name)); // Save save_last_project(tool_name, project_id).expect("Failed to save project"); // Load let loaded = load_last_project(tool_name).expect("Failed to load project"); assert_eq!(loaded, project_id); // Cleanup let _ = std::fs::remove_dir_all(tool_cache_dir(tool_name)); } }