xbp 0.9.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfo {
    pub path: PathBuf,
    pub name: String,
    pub last_accessed: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Profile {
    pub last_project_path: Option<PathBuf>,
    #[serde(default)]
    pub recent_projects: Vec<ProjectInfo>,
}

impl Profile {
    pub fn get_profile_path() -> Result<PathBuf, String> {
        let config_dir = if cfg!(target_os = "windows") {
            dirs::config_dir()
                .ok_or_else(|| "Could not determine config directory".to_string())?
        } else {
            dirs::config_dir()
                .ok_or_else(|| "Could not determine config directory".to_string())?
        };

        let xbp_config_dir = config_dir.join("xbp");
        if !xbp_config_dir.exists() {
            fs::create_dir_all(&xbp_config_dir)
                .map_err(|e| format!("Failed to create config directory: {}", e))?;
        }

        Ok(xbp_config_dir.join("profile.yaml"))
    }

    pub fn load() -> Result<Self, String> {
        let profile_path = Self::get_profile_path()?;

        if !profile_path.exists() {
            return Ok(Profile::default());
        }

        let content = fs::read_to_string(&profile_path)
            .map_err(|e| format!("Failed to read profile: {}", e))?;

        serde_yaml::from_str(&content)
            .map_err(|e| format!("Failed to parse profile: {}", e))
    }

    pub fn save(&self) -> Result<(), String> {
        let profile_path = Self::get_profile_path()?;

        let yaml = serde_yaml::to_string(self)
            .map_err(|e| format!("Failed to serialize profile: {}", e))?;

        fs::write(&profile_path, yaml)
            .map_err(|e| format!("Failed to write profile: {}", e))
    }

    pub fn update_last_project(&mut self, path: PathBuf, name: String) {
        self.last_project_path = Some(path.clone());

        let now = Utc::now();
        
        if let Some(existing) = self.recent_projects.iter_mut().find(|p| p.path == path) {
            existing.last_accessed = now;
            existing.name = name;
        } else {
            self.recent_projects.push(ProjectInfo {
                path,
                name,
                last_accessed: now,
            });
        }

        self.recent_projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
        
        if self.recent_projects.len() > 10 {
            self.recent_projects.truncate(10);
        }
    }
}

pub fn find_all_xbp_projects() -> Vec<ProjectInfo> {
    let mut projects = Vec::new();
    let search_dirs = get_search_directories();

    for search_dir in search_dirs {
        if !search_dir.exists() {
            continue;
        }

        for entry in WalkDir::new(&search_dir)
            .max_depth(5)
            .follow_links(false)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();
            
            if path.file_name() == Some(std::ffi::OsStr::new(".xbp")) && path.is_dir() {
                let xbp_json = path.join("xbp.json");
                if xbp_json.exists() {
                    if let Some(parent) = path.parent() {
                        let name = extract_project_name(&xbp_json.as_path())
                            .unwrap_or_else(|| {
                                parent
                                    .file_name()
                                    .and_then(|n| n.to_str())
                                    .unwrap_or("unknown")
                                    .to_string()
                            });

                        projects.push(ProjectInfo {
                            path: parent.to_path_buf(),
                            name,
                            last_accessed: Utc::now(),
                        });
                    }
                }
            } else if path.file_name() == Some(std::ffi::OsStr::new("xbp.json")) && path.is_file() {
                if let Some(parent) = path.parent() {
                    if !parent.join(".xbp").join("xbp.json").exists() {
                        let name = extract_project_name(path.as_ref())
                            .unwrap_or_else(|| {
                                parent
                                    .file_name()
                                    .and_then(|n| n.to_str())
                                    .unwrap_or("unknown")
                                    .to_string()
                            });

                        projects.push(ProjectInfo {
                            path: parent.to_path_buf(),
                            name,
                            last_accessed: Utc::now(),
                        });
                    }
                }
            }
        }
    }

    projects.sort_by(|a, b| a.name.cmp(&b.name));
    projects.dedup_by(|a, b| a.path == b.path);

    projects
}

fn get_search_directories() -> Vec<PathBuf> {
    let mut dirs = Vec::new();

    if let Some(home) = dirs::home_dir() {
        dirs.push(home.join("projects"));
        dirs.push(home.join("dev"));
        dirs.push(home.join("Documents"));
        dirs.push(home.join("src"));
        dirs.push(home.clone());
    }

    if cfg!(target_os = "windows") {
        if let Ok(current_dir) = std::env::current_dir() {
            if let Some(root) = current_dir.ancestors().last() {
                dirs.push(root.to_path_buf());
            }
        }
    }

    dirs
}

fn extract_project_name(xbp_json_path: &std::path::Path) -> Option<String> {
    let content = fs::read_to_string(xbp_json_path).ok()?;
    let json: serde_json::Value = serde_json::from_str(&content).ok()?;
    json.get("project_name")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

pub fn rank_projects_by_proximity(
    mut projects: Vec<ProjectInfo>,
    current_dir: PathBuf,
) -> Vec<ProjectInfo> {
    projects.sort_by_key(|project| {
        let common_components = current_dir
            .components()
            .zip(project.path.components())
            .take_while(|(a, b)| a == b)
            .count();
        
        std::cmp::Reverse(common_components)
    });

    projects
}