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)
236 lines
7.2 KiB
Rust
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));
|
|
}
|
|
}
|