sklink 0.2.2

Install skills into platform directories via a local store and symlinks
use std::path::{Path, PathBuf};

use crate::error::AppError;

pub fn resolve_path(raw: &str, base_dir: &Path) -> Result<PathBuf, AppError> {
    let expanded = expand_tilde(raw)?;
    if expanded.is_relative() {
        Ok(base_dir.join(expanded))
    } else {
        Ok(expanded)
    }
}

fn expand_tilde(raw: &str) -> Result<PathBuf, AppError> {
    if raw == "~" {
        return home_dir();
    }

    if let Some(rest) = raw.strip_prefix("~/") {
        return Ok(home_dir()?.join(rest));
    }

    Ok(PathBuf::from(raw))
}

fn home_dir() -> Result<PathBuf, AppError> {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .ok_or(AppError::HomeMissing)
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;
    use std::sync::Mutex;

    use super::*;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct EnvVarGuard {
        key: &'static str,
        old: Option<OsString>,
    }

    impl Drop for EnvVarGuard {
        fn drop(&mut self) {
            if let Some(old) = self.old.take() {
                std::env::set_var(self.key, old);
            } else {
                std::env::remove_var(self.key);
            }
        }
    }

    fn set_env_var(key: &'static str, value: Option<&str>) -> EnvVarGuard {
        let old = std::env::var_os(key);
        match value {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        EnvVarGuard { key, old }
    }

    #[test]
    fn resolve_path_joins_relative_path_to_base_dir() {
        let base = Path::new("/base");
        let out = resolve_path("skills/foo", base).unwrap();
        assert_eq!(out, PathBuf::from("/base/skills/foo"));
    }

    #[test]
    fn resolve_path_keeps_absolute_path() {
        let base = Path::new("/base");
        let out = resolve_path("/abs/path", base).unwrap();
        assert_eq!(out, PathBuf::from("/abs/path"));
    }

    #[test]
    fn resolve_path_expands_tilde_and_then_joins_relative() {
        let _lock = ENV_LOCK.lock().unwrap();
        let _home = set_env_var("HOME", Some("/home/tester"));

        let base = Path::new("/base");
        let out = resolve_path("~/dir/file", base).unwrap();
        assert_eq!(out, PathBuf::from("/home/tester/dir/file"));
    }

    #[test]
    fn resolve_path_expands_bare_tilde() {
        let _lock = ENV_LOCK.lock().unwrap();
        let _home = set_env_var("HOME", Some("/home/tester"));

        let base = Path::new("/base");
        let out = resolve_path("~", base).unwrap();
        assert_eq!(out, PathBuf::from("/home/tester"));
    }

    #[test]
    fn resolve_path_returns_error_when_home_missing_for_tilde() {
        let _lock = ENV_LOCK.lock().unwrap();
        let _home = set_env_var("HOME", None);

        let base = Path::new("/base");
        let err = resolve_path("~/dir", base).unwrap_err();
        assert!(matches!(err, AppError::HomeMissing));
    }
}