claude-profiles 0.9.0

Launch claude with only the plugins, skills, and MCP servers a profile defines
use std::ffi::OsString;
use std::path::PathBuf;

// HOME first so Git Bash/MSYS sessions and test overrides keep working; USERPROFILE is
// what plain PowerShell and cmd.exe on Windows provide.
pub fn home_from(home: Option<OsString>, userprofile: Option<OsString>) -> Option<PathBuf> {
    [home, userprofile]
        .into_iter()
        .flatten()
        .find(|v| !v.is_empty())
        .map(PathBuf::from)
}

pub struct Paths {
    pub home: PathBuf,
}

impl Paths {
    pub fn from_home(home: PathBuf) -> Paths {
        Paths { home }
    }

    pub fn detect() -> anyhow::Result<Paths> {
        let home = home_from(std::env::var_os("HOME"), std::env::var_os("USERPROFILE"))
            .ok_or_else(|| anyhow::anyhow!("cannot find your home directory: set HOME or USERPROFILE"))?;
        Ok(Paths::from_home(home))
    }

    pub fn user_profiles_dir(&self) -> PathBuf {
        self.home.join(".claude-profiles")
    }

    // Engine-managed copy of the profiles shipped with the binary; seeded on first run.
    pub fn bundled_profiles_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("bundled")
    }

    pub fn claude_skills_dir(&self) -> PathBuf {
        self.home.join(".claude").join("skills")
    }

    // Claude Code's own global settings file — used by `statusline install` to add a statusLine entry.
    pub fn claude_settings_path(&self) -> PathBuf {
        self.home.join(".claude").join("settings.json")
    }

    pub fn statusline_backups_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("statusline-backups")
    }

    // Phase 2/3: profile locking (concurrent-launch guard) will use this.
    #[allow(dead_code)]
    pub fn locks_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("locks")
    }

    #[allow(dead_code)]
    pub fn runs_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("runs")
    }

    #[allow(dead_code)]
    pub fn worktrees_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("worktrees")
    }

    #[allow(dead_code)]
    pub fn bundled_plugins_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("bundled-plugins")
    }

    pub fn index_cache_dir(&self) -> PathBuf {
        self.user_profiles_dir().join(".index-cache")
    }

    pub fn index_file(&self) -> PathBuf {
        self.index_cache_dir().join("index.json")
    }

    pub fn index_repos_dir(&self) -> PathBuf {
        self.index_cache_dir().join("repos")
    }

    pub fn marketplaces_seed_file(&self) -> PathBuf {
        self.user_profiles_dir().join("marketplaces.txt")
    }

    pub fn store_dir(&self) -> PathBuf {
        self.user_profiles_dir().join("store")
    }

    pub fn marketplace_clone_dir(&self, name: &str) -> PathBuf {
        self.store_dir().join("marketplaces").join(name)
    }

    pub fn external_marketplace_dir(&self, owner: &str, repo: &str) -> PathBuf {
        self.store_dir().join("marketplaces").join("_external").join(format!("{owner}--{repo}"))
    }

    pub fn profile_vendor_dir(&self, profile_key: &str) -> PathBuf {
        self.store_dir().join(profile_key).join("vendor")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn home_prefers_home_over_userprofile() {
        let got = home_from(Some("/home/a".into()), Some(r"C:\Users\a".into()));
        assert_eq!(got, Some(PathBuf::from("/home/a")));
    }

    #[test]
    fn home_falls_back_to_userprofile() {
        assert_eq!(home_from(None, Some(r"C:\Users\a".into())), Some(PathBuf::from(r"C:\Users\a")));
    }

    #[test]
    fn empty_home_falls_back_to_userprofile() {
        assert_eq!(home_from(Some("".into()), Some(r"C:\Users\a".into())), Some(PathBuf::from(r"C:\Users\a")));
    }

    #[test]
    fn no_home_at_all_is_none() {
        assert_eq!(home_from(None, None), None);
        assert_eq!(home_from(Some("".into()), Some("".into())), None);
    }

    #[test]
    fn derives_paths_from_home() {
        let p = Paths::from_home(PathBuf::from("/h"));
        assert_eq!(p.user_profiles_dir(), PathBuf::from("/h/.claude-profiles"));
        assert_eq!(p.claude_skills_dir(), PathBuf::from("/h/.claude/skills"));
        assert_eq!(p.claude_settings_path(), PathBuf::from("/h/.claude/settings.json"));
        assert_eq!(p.locks_dir(), PathBuf::from("/h/.claude-profiles/locks"));
    }

    #[test]
    fn derives_bundled_profiles_dir() {
        let p = Paths::from_home(PathBuf::from("/h"));
        assert_eq!(p.bundled_profiles_dir(), PathBuf::from("/h/.claude-profiles/bundled"));
    }

    #[test]
    fn derives_index_paths_from_home() {
        let p = Paths::from_home(PathBuf::from("/h"));
        assert_eq!(p.index_cache_dir(), PathBuf::from("/h/.claude-profiles/.index-cache"));
        assert_eq!(p.index_file(), PathBuf::from("/h/.claude-profiles/.index-cache/index.json"));
        assert_eq!(p.index_repos_dir(), PathBuf::from("/h/.claude-profiles/.index-cache/repos"));
        assert_eq!(p.marketplaces_seed_file(), PathBuf::from("/h/.claude-profiles/marketplaces.txt"));
    }

    #[test]
    fn derives_store_and_vendor_paths() {
        let p = Paths::from_home(PathBuf::from("/h"));
        assert_eq!(p.store_dir(), PathBuf::from("/h/.claude-profiles/store"));
        assert_eq!(
            p.marketplace_clone_dir("superpowers-marketplace"),
            PathBuf::from("/h/.claude-profiles/store/marketplaces/superpowers-marketplace")
        );
        assert_eq!(
            p.external_marketplace_dir("fuzzyalej", "openpowers"),
            PathBuf::from("/h/.claude-profiles/store/marketplaces/_external/fuzzyalej--openpowers")
        );
        assert_eq!(
            p.profile_vendor_dir("rust-developer"),
            PathBuf::from("/h/.claude-profiles/store/rust-developer/vendor")
        );
    }

    #[test]
    fn derives_statusline_backups_dir() {
        let p = Paths::from_home(PathBuf::from("/h"));
        assert_eq!(
            p.statusline_backups_dir(),
            PathBuf::from("/h/.claude-profiles/statusline-backups")
        );
    }
}