syntaxis/shared/rust/project_detection.rs
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

236 lines
7.2 KiB
Rust

//! 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<DetectedProject> {
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<P: AsRef<Path>>(
path: P,
config_filename: &str,
) -> Option<DetectedProject> {
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<String> {
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<String, Box<dyn std::error::Error>> {
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));
}
}