xbp 10.7.0

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() {
                    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));

    let mut deduplicated = Vec::new();
    let mut seen_paths = std::collections::HashSet::new();

    for project in projects {
        let canonical_path = project
            .path
            .canonicalize()
            .unwrap_or_else(|_| project.path.clone());

        let should_skip = seen_paths.iter().any(|seen: &PathBuf| {
            let seen_canonical = seen.canonicalize().unwrap_or_else(|_| seen.clone());

            if canonical_path == seen_canonical {
                return true;
            }

            if let (Some(seen_parent), Some(current_parent)) =
                (seen_canonical.parent(), canonical_path.parent())
            {
                if seen_parent == canonical_path
                    && seen_canonical.file_name() == Some(std::ffi::OsStr::new(".xbp"))
                {
                    return true;
                }
                if current_parent == seen_canonical
                    && canonical_path.file_name() == Some(std::ffi::OsStr::new(".xbp"))
                {
                    return true;
                }
            }

            false
        });

        if !should_skip {
            seen_paths.insert(canonical_path);
            deduplicated.push(project);
        }
    }

    deduplicated
}

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
}