repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

pub fn find_upward(start: &Path, name: &str) -> Option<PathBuf> {
    let mut current = if start.is_file() {
        start.parent()?.to_path_buf()
    } else {
        start.to_path_buf()
    };

    loop {
        let candidate = current.join(name);

        if candidate.exists() {
            return Some(candidate);
        }

        if !current.pop() {
            return None;
        }
    }
}

pub fn has_github_workflows(workflows_dir: &Path) -> bool {
    let Ok(entries) = fs::read_dir(workflows_dir) else {
        return false;
    };

    entries.filter_map(Result::ok).any(|entry| {
        let path = entry.path();

        path.is_file()
            && path
                .extension()
                .and_then(|extension| extension.to_str())
                .is_some_and(|extension| matches!(extension, "yml" | "yaml"))
    })
}

pub fn detect_package_managers(root: &Path) -> Vec<String> {
    let mut managers = Vec::new();

    if root.join("Cargo.toml").is_file() {
        managers.push("Cargo".to_string());
    }

    if root.join("package.json").is_file() {
        if root.join("pnpm-lock.yaml").is_file() {
            managers.push("pnpm".to_string());
        } else if root.join("yarn.lock").is_file() {
            managers.push("Yarn".to_string());
        } else if root.join("bun.lockb").is_file() || root.join("bun.lock").is_file() {
            managers.push("Bun".to_string());
        } else if root.join("package-lock.json").is_file() {
            managers.push("npm".to_string());
        } else {
            managers.push("Node package.json".to_string());
        }
    }

    if root.join("pyproject.toml").is_file() {
        managers.push("Python pyproject".to_string());
    } else if root.join("requirements.txt").is_file() {
        managers.push("pip requirements".to_string());
    }

    if root.join("go.mod").is_file() {
        managers.push("Go modules".to_string());
    }

    if root.join("Gemfile").is_file() {
        managers.push("Bundler".to_string());
    }

    if root.join("composer.json").is_file() {
        managers.push("Composer".to_string());
    }

    if root.join("pom.xml").is_file()
        || root.join("build.gradle").is_file()
        || root.join("build.gradle.kts").is_file()
    {
        managers.push("JVM build".to_string());
    }

    if root.join("Package.swift").is_file() {
        managers.push("Swift Package Manager".to_string());
    }

    managers
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn detects_pnpm_package_manager() {
        let dir = tempdir().expect("tempdir should be created");
        fs::write(dir.path().join("package.json"), "{}").expect("package.json should be written");
        fs::write(dir.path().join("pnpm-lock.yaml"), "lockfileVersion: 9")
            .expect("pnpm lockfile should be written");

        let managers = detect_package_managers(dir.path());

        assert_eq!(managers, vec!["pnpm"]);
    }
}