repopilot 0.19.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::WorkspacePackage;
use std::path::Path;

pub(super) fn from_cargo_workspace(root: &Path) -> Vec<WorkspacePackage> {
    let content = match std::fs::read_to_string(root.join("Cargo.toml")) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };
    let value: toml::Value = match toml::from_str(&content) {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };

    let members = value
        .get("workspace")
        .and_then(|w| w.get("members"))
        .and_then(|m| m.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_string))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();

    if members.is_empty() {
        return Vec::new();
    }

    let mut packages = Vec::new();
    for member in &members {
        let trimmed = member.trim_end_matches('/');
        if trimmed.contains('*') {
            let prefix = trimmed.trim_end_matches("/*").trim_end_matches("/**");
            let base = root.join(prefix);
            let Ok(entries) = std::fs::read_dir(&base) else {
                continue;
            };
            for entry in entries.filter_map(Result::ok) {
                let path = entry.path();
                if path.is_dir()
                    && path.join("Cargo.toml").is_file()
                    && let Some(name) = package_name_from_path(&path)
                {
                    packages.push(WorkspacePackage {
                        name,
                        root: path,
                        exposes_subpath_exports: false,
                    });
                }
            }
        } else {
            let path = root.join(trimmed);
            if path.join("Cargo.toml").is_file()
                && let Some(name) = package_name_from_path(&path)
            {
                packages.push(WorkspacePackage {
                    name,
                    root: path,
                    exposes_subpath_exports: false,
                });
            }
        }
    }
    packages
}

fn package_name_from_path(path: &Path) -> Option<String> {
    let content = std::fs::read_to_string(path.join("Cargo.toml")).ok()?;
    let value: toml::Value = toml::from_str(&content).ok()?;
    value
        .get("package")
        .and_then(|p| p.get("name"))
        .and_then(|n| n.as_str())
        .map(str::to_string)
        .or_else(|| {
            path.file_name()
                .and_then(|n| n.to_str())
                .map(str::to_string)
        })
}