minni 0.1.1

Local memory, task, and codebase indexing tool for AI agents
Documentation
use crate::db::Database;
use crate::models::downloader::{
    get_dense_model_dir, get_model_dir, is_dense_model_installed, is_model_installed,
    DENSE_MODEL_ID, MODEL_ID,
};
use anyhow::Result;
use colored::Colorize;
use std::env;

const PASS: &str = "";
const FAIL: &str = "";

/// Run all diagnostic checks. Returns true if every check passed.
pub fn run() -> Result<bool> {
    let mut all_ok = true;

    // 1. Binary version
    let version = env!("CARGO_PKG_VERSION");
    println!("{} Binary version: {}", PASS.green(), version.cyan());

    let project_root = env::current_dir()?;

    // 2. .minni/ directory exists and is writable
    let minni_dir = project_root.join(".minni");
    if minni_dir.exists() {
        // Probe write access by attempting a temp file
        let probe_path = minni_dir.join(".write_probe");
        match std::fs::write(&probe_path, b"") {
            Ok(_) => {
                let _ = std::fs::remove_file(&probe_path);
                println!("{} .minni/ directory: exists and writable", PASS.green());
            }
            Err(e) => {
                println!("{} .minni/ directory: exists but NOT writable", FAIL.red());
                println!("    Fix: Check permissions on {}", minni_dir.display());
                println!("    Error: {}", e);
                all_ok = false;
            }
        }
    } else {
        println!("{} .minni/ directory: not found", FAIL.red());
        println!(
            "    Fix: Run {} to initialise minni in this directory",
            "minni init".cyan()
        );
        all_ok = false;
    }

    // 3. SQLite DB opens successfully
    if Database::minni_dir_exists(&project_root) {
        match Database::open(&project_root) {
            Ok(_) => println!("{} SQLite database: opens successfully", PASS.green()),
            Err(e) => {
                println!("{} SQLite database: failed to open", FAIL.red());
                println!(
                    "    Fix: Try removing .minni/ and running {} again",
                    "minni init".cyan()
                );
                println!("    Error: {}", e);
                all_ok = false;
            }
        }
    } else {
        println!(
            "{} SQLite database: not found (run {} first)",
            FAIL.red(),
            "minni init".cyan()
        );
        all_ok = false;
    }

    // 4. BM25 index directory exists and is non-empty
    let bm25_dir = minni_dir.join("bm25_index");
    if bm25_dir.exists() {
        match std::fs::read_dir(&bm25_dir) {
            Ok(mut entries) => {
                if entries.next().is_some() {
                    println!("{} BM25 index: exists and non-empty", PASS.green());
                } else {
                    println!("{} BM25 index: directory exists but is empty", FAIL.red());
                    println!(
                        "    Fix: Run {} to build the search index",
                        "minni index".cyan()
                    );
                    all_ok = false;
                }
            }
            Err(e) => {
                println!("{} BM25 index: cannot read index directory", FAIL.red());
                println!("    Error: {}", e);
                println!("    Fix: Check permissions on {}", bm25_dir.display());
                all_ok = false;
            }
        }
    } else {
        println!("{} BM25 index: not found", FAIL.red());
        println!(
            "    Fix: Run {} to build the search index",
            "minni index".cyan()
        );
        all_ok = false;
    }

    // 5. Reranker model — three states: present / downloadable / missing
    if is_model_installed() {
        println!("{} Reranker model: installed", PASS.green());
    } else if !MODEL_ID.is_empty() {
        // Model ID is configured → can be downloaded on demand; not a failure
        let model_dir = get_model_dir().unwrap_or_else(|_| std::path::PathBuf::from("<unknown>"));
        println!(
            "{} Reranker model: not installed (will download on first search)",
            "~".yellow()
        );
        println!("    Expected path: {}", model_dir.display());
    } else {
        let model_dir = get_model_dir().unwrap_or_else(|_| std::path::PathBuf::from("<unknown>"));
        println!(
            "{} Reranker model: missing and no download configured",
            FAIL.red()
        );
        println!("    Expected path: {}", model_dir.display());
        all_ok = false;
    }

    // 6. Dense model — three states: present / downloadable / missing
    if is_dense_model_installed() {
        println!("{} Dense model: installed", PASS.green());
    } else if !DENSE_MODEL_ID.is_empty() {
        // Model ID is configured → can be downloaded on demand; not a failure
        let model_dir =
            get_dense_model_dir().unwrap_or_else(|_| std::path::PathBuf::from("<unknown>"));
        println!(
            "{} Dense model: not installed (will download on first index/search)",
            "~".yellow()
        );
        println!("    Expected path: {}", model_dir.display());
    } else {
        let model_dir =
            get_dense_model_dir().unwrap_or_else(|_| std::path::PathBuf::from("<unknown>"));
        println!(
            "{} Dense model: missing and no download configured",
            FAIL.red()
        );
        println!("    Expected path: {}", model_dir.display());
        all_ok = false;
    }

    // 7. ONNX runtime loads successfully (try loading the reranker if present)
    check_onnx_runtime(&mut all_ok);

    println!();
    if all_ok {
        println!("{} All checks passed.", "✓ Doctor:".green().bold());
    } else {
        println!(
            "{} Some checks failed — see details above.",
            "✗ Doctor:".red().bold()
        );
    }

    Ok(all_ok)
}

fn check_onnx_runtime(all_ok: &mut bool) {
    if !is_model_installed() {
        // Can't test ONNX without a model; skip rather than fail
        println!(
            "{} ONNX runtime: skipped (no model installed — install reranker model first)",
            "~".yellow()
        );
        return;
    }

    match get_model_dir() {
        Ok(model_dir) => {
            let model_file = model_dir.join("model.onnx");
            match try_load_onnx_session(&model_file) {
                Ok(()) => println!("{} ONNX runtime: loads successfully", PASS.green()),
                Err(e) => {
                    println!("{} ONNX runtime: failed to load", FAIL.red());
                    println!("    Error: {}", e);
                    println!(
                        "    Fix: Delete the model at {} and let it re-download on next search.",
                        model_dir.display()
                    );
                    *all_ok = false;
                }
            }
        }
        Err(e) => {
            println!(
                "{} ONNX runtime: could not determine model directory",
                FAIL.red()
            );
            println!("    Error: {}", e);
            *all_ok = false;
        }
    }
}

fn try_load_onnx_session(model_file: &std::path::Path) -> Result<()> {
    use ort::session::Session;
    Session::builder()?.commit_from_file(model_file)?;
    Ok(())
}