iforgor 0.3.3

The CLI tool for all those commands you forget about
Documentation
use std::path::{Path, PathBuf};

const IFORGOR_DIR: &str = ".iforgor";

/// Walk from `start_dir` upward to root, collecting all `.iforgor/` directories found.
/// Also includes `~/.iforgor/` (global). Ordered from most specific (closest) to least.
pub fn discover_iforgor_dirs(start_dir: &Path) -> Vec<PathBuf> {
    let mut dirs = Vec::new();
    let mut current = start_dir.to_path_buf();

    loop {
        let candidate = current.join(IFORGOR_DIR);
        if candidate.is_dir() {
            dirs.push(candidate);
        }

        if !current.pop() {
            break;
        }
    }

    // Add ~/.iforgor/ if not already discovered (e.g., CWD is not under home).
    if let Some(home) = home::home_dir() {
        let global = home.join(IFORGOR_DIR);
        if global.is_dir() && !dirs.contains(&global) {
            dirs.push(global);
        }
    }

    dirs
}

/// Find all domain TOML files in an `.iforgor/` directory.
/// - Recursively scans subdirectories (nesting support).
/// - Skips files whose name starts with `.` (reserved for config).
pub fn find_domain_files(iforgor_dir: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    collect_toml_files(iforgor_dir, &mut files);
    files.sort();
    files
}

fn collect_toml_files(dir: &Path, files: &mut Vec<PathBuf>) {
    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_toml_files(&path, files);
        } else if path.extension().is_some_and(|ext| ext == "toml") {
            // Skip dot-prefixed files (e.g., .config.toml).
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                if !name.starts_with('.') {
                    files.push(path);
                }
            }
        }
    }
}

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

    #[test]
    fn discover_finds_iforgor_dirs_closest_first() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();

        // root/a/b/.iforgor/
        // root/a/.iforgor/
        let deep = root.join("a/b");
        fs::create_dir_all(deep.join(".iforgor")).unwrap();
        fs::create_dir_all(root.join("a/.iforgor")).unwrap();

        let dirs = discover_iforgor_dirs(&deep);

        // Closest first: a/b/.iforgor before a/.iforgor
        assert!(dirs.len() >= 2);
        assert_eq!(dirs[0], deep.join(".iforgor"));
        assert_eq!(dirs[1], root.join("a/.iforgor"));
    }

    #[test]
    fn discover_no_iforgor_dirs() {
        let tmp = tempfile::tempdir().unwrap();
        let dirs = discover_iforgor_dirs(tmp.path());
        // May include ~/.iforgor/ if it exists, but no dirs from tmp
        for d in &dirs {
            assert!(!d.starts_with(tmp.path()));
        }
    }

    #[test]
    fn find_domain_files_recursive() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();

        // Create domain files
        fs::write(root.join("docker.toml"), "").unwrap();
        fs::create_dir_all(root.join("ci")).unwrap();
        fs::write(root.join("ci/deploy.toml"), "").unwrap();

        let files = find_domain_files(root);
        assert_eq!(files.len(), 2);
        assert!(files.contains(&root.join("ci/deploy.toml")));
        assert!(files.contains(&root.join("docker.toml")));
    }

    #[test]
    fn find_domain_files_skips_dot_prefixed() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();

        fs::write(root.join(".config.toml"), "").unwrap();
        fs::write(root.join("valid.toml"), "").unwrap();

        let files = find_domain_files(root);
        assert_eq!(files.len(), 1);
        assert_eq!(files[0], root.join("valid.toml"));
    }

    #[test]
    fn find_domain_files_skips_non_toml() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();

        fs::write(root.join("readme.md"), "").unwrap();
        fs::write(root.join("script.sh"), "").unwrap();
        fs::write(root.join("valid.toml"), "").unwrap();

        let files = find_domain_files(root);
        assert_eq!(files.len(), 1);
    }

    #[test]
    fn find_domain_files_empty_dir() {
        let tmp = tempfile::tempdir().unwrap();
        let files = find_domain_files(tmp.path());
        assert!(files.is_empty());
    }
}