cx-cli 0.7.0

Semantic code navigation for AI agents
use std::path::PathBuf;
use crate::language::{supported_languages, download_names_for};

pub fn cx_cache_dir() -> PathBuf {
    if let Ok(dir) = std::env::var("CX_CACHE_DIR") {
        return PathBuf::from(dir);
    }
    dirs::cache_dir()
        .unwrap_or_else(|| PathBuf::from(".cache"))
        .join("cx")
}

pub fn grammar_cache_dir() -> PathBuf {
    cx_cache_dir().join("grammars")
}

pub fn add(languages: &[String]) -> i32 {
    if languages.is_empty() {
        eprintln!("cx: specify at least one language, e.g.: cx lang add rust typescript");
        return 1;
    }

    let supported = supported_languages();
    for lang in languages {
        if !supported.contains(&lang.as_str()) {
            eprintln!("cx: unknown language '{}' — supported: {}", lang, supported.join(", "));
            return 1;
        }
    }

    // Expand to actual download names (e.g. "typescript" → ["typescript", "tsx"])
    let mut to_download: Vec<&str> = Vec::new();
    for lang in languages {
        for name in download_names_for(lang) {
            if !to_download.contains(&name) {
                to_download.push(name);
            }
        }
    }

    eprintln!("cx: downloading grammars: {}", to_download.join(", "));

    match tree_sitter_language_pack::download(&to_download) {
        Ok(_) => {
            eprintln!("cx: installed {} grammar(s)", to_download.len());
            0
        }
        Err(e) => {
            eprintln!("cx: download failed: {e}");
            eprintln!("cx: check your network connection and try again");
            1
        }
    }
}

pub fn remove(languages: &[String]) -> i32 {
    if languages.is_empty() {
        eprintln!("cx: specify at least one language, e.g.: cx lang remove rust");
        return 1;
    }

    let libs_dir = match tree_sitter_language_pack::cache_dir() {
        Ok(dir) => dir,
        Err(e) => {
            eprintln!("cx: failed to find cache directory: {e}");
            return 1;
        }
    };

    for lang in languages {
        let names = download_names_for(lang);
        let names = if names.is_empty() { vec![lang.as_str()] } else { names };
        let mut removed_any = false;
        for name in &names {
            let lib_prefix = format!("libtree_sitter_{name}");
            if let Ok(entries) = std::fs::read_dir(&libs_dir) {
                for entry in entries.flatten() {
                    let fname = entry.file_name();
                    let fname_str = fname.to_string_lossy();
                    if fname_str.starts_with(&lib_prefix) && (fname_str.ends_with(".so") || fname_str.ends_with(".dylib") || fname_str.ends_with(".dll")) {
                        let _ = std::fs::remove_file(entry.path());
                        removed_any = true;
                    }
                }
            }
        }

        if removed_any {
            eprintln!("cx: removed {lang} grammar");
        } else {
            eprintln!("cx: {lang} grammar not found in cache");
        }
    }
    0
}

pub fn list() -> i32 {
    let supported = supported_languages();
    let installed = tree_sitter_language_pack::downloaded_languages();

    for lang in &supported {
        let names = download_names_for(lang);
        let is_installed = names.iter().all(|n| installed.iter().any(|i| i == n));
        let marker = if is_installed { "[installed]" } else { "[missing]" };
        println!("{lang:<15} {marker}");
    }
    eprintln!("\nNeed another language? Open an issue: https://github.com/ind-igo/cx/issues/new?template=language-request.yml");
    0
}