sac-cli 0.1.0

Terminal-based AI coding agent — fork of NAC with extended backend support and context management
Documentation
use super::*;

#[derive(Clone, Debug)]
pub(super) struct CommandSource {
    pub(super) root: PathBuf,
    pub(super) precedence: u8,
}

pub(super) fn discover_command_sources(workspace_dir: Option<&Path>) -> Vec<CommandSource> {
    let mut sources = Vec::new();

    if let Some(workspace_dir) = workspace_dir {
        let root =
            find_project_root(workspace_dir).unwrap_or_else(|| workspace_dir.to_path_buf());

        sources.push(CommandSource {
            root: root.join(".sac").join("commands"),
            precedence: 0,
        });
        sources.push(CommandSource {
            root: root.join(".opencode").join("commands"),
            precedence: 1,
        });
        sources.push(CommandSource {
            root: root.join(".opencode").join("command"),
            precedence: 2,
        });
    }

    if let Some(sac_home) = sac_home_dir() {
        sources.push(CommandSource {
            root: sac_home.join("commands"),
            precedence: 3,
        });
    }

    if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
        let config_base = std::env::var_os("XDG_CONFIG_HOME")
            .map(PathBuf::from)
            .unwrap_or_else(|| home.join(".config"));
        sources.push(CommandSource {
            root: config_base.join("opencode").join("commands"),
            precedence: 4,
        });
        sources.push(CommandSource {
            root: config_base.join("opencode").join("command"),
            precedence: 5,
        });
    }

    sources.sort_by_key(|source| source.precedence);
    sources
}

pub(super) fn discover_command_files(root: &Path) -> Vec<(String, PathBuf)> {
    if !root.is_dir() {
        return Vec::new();
    }

    let mut found = Vec::new();
    let mut queue = std::collections::VecDeque::from([(root.to_path_buf(), 0usize)]);
    let mut scanned = 0usize;

    while let Some((dir, depth)) = queue.pop_front() {
        if scanned >= MAX_SCAN_DIRS {
            break;
        }
        scanned += 1;

        let entries = match std::fs::read_dir(&dir) {
            Ok(entries) => entries,
            Err(_) => continue,
        };

        for entry in entries.flatten() {
            let path = entry.path();

            if path.is_dir() {
                if depth < MAX_SCAN_DEPTH {
                    let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
                    if !matches!(
                        name,
                        ".git" | "node_modules" | "target" | ".venv" | "__pycache__"
                    ) {
                        queue.push_back((path, depth + 1));
                    }
                }
                continue;
            }

            if path.extension().and_then(|e| e.to_str()) != Some("md") {
                continue;
            }

            if let Ok(relative) = path.strip_prefix(root) {
                let name = relative
                    .with_extension("")
                    .to_string_lossy()
                    .replace(std::path::MAIN_SEPARATOR, "/");
                if !name.is_empty() {
                    found.push((name, path));
                }
            }
        }
    }

    found.sort_by(|a, b| a.0.cmp(&b.0));
    found
}

fn find_project_root(workspace_dir: &Path) -> Option<PathBuf> {
    let mut current = Some(workspace_dir);
    while let Some(dir) = current {
        let git_marker = dir.join(".git");
        if git_marker.is_dir() || git_marker.is_file() {
            return Some(dir.to_path_buf());
        }
        current = dir.parent();
    }
    None
}