kodegraf-cli 0.1.3

Structural code intelligence for AI coding assistants — CLI
use anyhow::Result;
use kodegraf_core::config::Config;
use kodegraf_core::graph::GraphStore;
use kodegraf_core::{build, config};

/// SessionStart hook: print graph stats so Claude knows the graph exists.
///
/// Auto-builds the graph if it's missing or empty — this is the key to
/// zero-config: user runs `kodegraf install`, opens Claude Code, and
/// this hook ensures the graph is ready before Claude starts working.
pub fn run() -> Result<()> {
    let repo_root = config::find_repo_root()?;
    let config_path = Config::config_path(&repo_root);
    let db_path = Config::graph_db_path(&repo_root);

    // Auto-init if not initialized (handles fresh repos after install)
    if !config_path.exists() {
        let config_dir = Config::config_dir(&repo_root);
        std::fs::create_dir_all(&config_dir)?;
        let cfg = Config::detect(&repo_root)?;
        cfg.save(&config_path)?;
    }

    // Open or create the database
    let store = GraphStore::open(&db_path)?;
    let stats = store.get_stats()?;

    // Auto-build if graph is empty (0 nodes)
    if stats.total_nodes == 0 {
        let cfg = Config::load(&config_path)?;
        eprintln!("Kodegraf: Building knowledge graph (first session)...");
        let result = build::full_build(&repo_root, &cfg, &store)?;
        eprintln!(
            "Kodegraf: Built — {} files, {} nodes, {} edges.",
            result.files_parsed, result.nodes_created, result.edges_created
        );
        // Re-read stats after build
        let stats = store.get_stats()?;
        print_greeting(&stats);
    } else {
        // Graph exists — check if HEAD changed since last build (incremental update)
        let current_ref = get_current_ref(&repo_root).unwrap_or_default();
        let stored_ref = store.get_metadata("git_ref").ok().flatten().unwrap_or_default();

        if !current_ref.is_empty() && current_ref != stored_ref {
            let cfg = Config::load(&config_path)?;
            eprintln!("Kodegraf: HEAD changed, updating graph...");
            let result = build::incremental_update(&repo_root, &cfg, &store, "HEAD~1")?;
            eprintln!(
                "Kodegraf: Updated {} files ({} nodes, {} edges).",
                result.files_parsed, result.nodes_created, result.edges_created
            );
        }

        print_greeting(&stats);
    }

    Ok(())
}

fn print_greeting(stats: &kodegraf_core::models::GraphStats) {
    println!(
        "Kodegraf knowledge graph: {} files, {} functions, {} classes, {} types, {} edges.",
        stats.files, stats.functions, stats.classes, stats.types, stats.total_edges
    );

    if !stats.languages.is_empty() {
        println!("Languages: {}.", stats.languages.join(", "));
    }

    println!();
    println!("ALWAYS use Kodegraf MCP tools (kodegraf_find, kodegraf_deps, kodegraf_check) BEFORE Grep/Glob/Read.");
}

fn get_current_ref(repo_root: &std::path::Path) -> Option<String> {
    let output = std::process::Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .current_dir(repo_root)
        .output()
        .ok()?;

    if output.status.success() {
        let branch = std::process::Command::new("git")
            .args(["branch", "--show-current"])
            .current_dir(repo_root)
            .output()
            .ok()?;
        let branch_str = String::from_utf8_lossy(&branch.stdout).trim().to_string();
        let sha_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
        Some(format!("{}@{}", branch_str, sha_str))
    } else {
        None
    }
}