cgz 2026.5.1

Local-first semantic code intelligence graph
Documentation
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use codegraph::types::SearchOptions;
use codegraph::{find_nearest_codegraph_root, is_initialized, CodeGraph};
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "cgz")]
#[command(about = "Code intelligence and knowledge graph for any codebase")]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    Init {
        path: Option<PathBuf>,
        #[arg(short, long)]
        index: bool,
    },
    Uninit {
        path: Option<PathBuf>,
        #[arg(short, long)]
        force: bool,
    },
    Index {
        path: Option<PathBuf>,
        #[arg(short, long)]
        force: bool,
        #[arg(short, long)]
        quiet: bool,
    },
    Sync {
        path: Option<PathBuf>,
        #[arg(short, long)]
        quiet: bool,
    },
    Status {
        path: Option<PathBuf>,
        #[arg(short, long)]
        json: bool,
    },
    Query {
        search: String,
        #[arg(short, long)]
        path: Option<PathBuf>,
        #[arg(short, long, default_value_t = 10)]
        limit: i64,
        #[arg(short, long)]
        json: bool,
    },
    Files {
        #[arg(short, long)]
        path: Option<PathBuf>,
        #[arg(short, long)]
        json: bool,
    },
    Context {
        task: String,
        #[arg(short, long)]
        path: Option<PathBuf>,
    },
    Affected {
        files: Vec<String>,
        #[arg(short, long)]
        path: Option<PathBuf>,
        #[arg(short, long)]
        json: bool,
    },
    Serve {
        #[arg(long)]
        mcp: bool,
        #[arg(short, long)]
        path: Option<PathBuf>,
    },
    Unlock {
        path: Option<PathBuf>,
    },
    Install,
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    match cli.command.unwrap_or(Command::Install) {
        Command::Init { path, index } => {
            let root = path.unwrap_or(std::env::current_dir()?);
            let mut cg = CodeGraph::init(&root)?;
            println!("Initialized in {}", cg.root().display());
            if index {
                let result = cg.index_all()?;
                print_index_result(&result);
            } else {
                println!("Run `cgz index` to index the project");
            }
        }
        Command::Uninit { path, force } => {
            let root = resolve_root(path)?;
            if !force {
                eprintln!(
                    "Refusing to remove {} without --force",
                    root.join(".codegraph").display()
                );
                std::process::exit(1);
            }
            std::fs::remove_dir_all(root.join(".codegraph"))?;
            println!("Removed CodeGraph data");
        }
        Command::Index { path, quiet, .. } => {
            let root = resolve_root(path)?;
            let mut cg = CodeGraph::open(root)?;
            let result = cg.index_all()?;
            if !quiet {
                print_index_result(&result);
            }
            if !result.success {
                std::process::exit(1);
            }
        }
        Command::Sync { path, quiet } => {
            let root = resolve_root(path)?;
            let mut cg = CodeGraph::open(root)?;
            let result = cg.sync()?;
            if !quiet {
                print_index_result(&result);
            }
        }
        Command::Status { path, json } => {
            let root = path.unwrap_or(std::env::current_dir()?);
            if !is_initialized(&root) && find_nearest_codegraph_root(&root).is_none() {
                if json {
                    println!(
                        "{}",
                        serde_json::json!({ "initialized": false, "projectPath": root })
                    );
                } else {
                    println!("CodeGraph not initialized in {}", root.display());
                }
                return Ok(());
            }
            let cg = CodeGraph::open(root)?;
            let stats = cg.stats()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&stats)?);
            } else {
                println!("CodeGraph Status");
                println!("Files: {}", stats.file_count);
                println!("Nodes: {}", stats.node_count);
                println!("Edges: {}", stats.edge_count);
                println!("DB Size: {} bytes", stats.db_size_bytes);
                println!("Files by Language:");
                for (lang, count) in stats.files_by_language {
                    println!("  {lang:<15} {count}");
                }
            }
        }
        Command::Query {
            search,
            path,
            limit,
            json,
        } => {
            let root = resolve_root(path)?;
            let cg = CodeGraph::open(root)?;
            let results = cg.search_nodes(
                &search,
                SearchOptions {
                    limit,
                    ..Default::default()
                },
            )?;
            if json {
                println!("{}", serde_json::to_string_pretty(&results)?);
            } else if results.is_empty() {
                println!("No results found for \"{}\"", search);
            } else {
                for r in results {
                    println!(
                        "{} {} {}:{}",
                        r.node.kind, r.node.name, r.node.file_path, r.node.start_line
                    );
                }
            }
        }
        Command::Files { path, json } => {
            let root = resolve_root(path)?;
            let cg = CodeGraph::open(root)?;
            let stats = cg.stats()?;
            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&stats.files_by_language)?
                );
            } else {
                for (lang, count) in stats.files_by_language {
                    println!("{lang}: {count}");
                }
            }
        }
        Command::Context { task, path } => {
            let root = resolve_root(path)?;
            let cg = CodeGraph::open(root)?;
            println!("{}", cg.build_context(&task, 20, true)?);
        }
        Command::Affected { files, path, json } => {
            let root = resolve_root(path)?;
            let cg = CodeGraph::open(root)?;
            let mut affected = std::collections::BTreeSet::new();
            for file in &files {
                if is_test_file(file) {
                    affected.insert(file.clone());
                    continue;
                }
                for dep in cg.get_file_dependents(file)? {
                    if is_test_file(&dep) {
                        affected.insert(dep);
                    }
                }
            }
            let affected: Vec<String> = affected.into_iter().collect();
            if json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&serde_json::json!({
                        "changedFiles": files,
                        "affectedTests": affected,
                    }))?
                );
            } else {
                for f in affected {
                    println!("{f}");
                }
            }
        }
        Command::Serve { mcp, path } => {
            if mcp {
                let mut server = codegraph::mcp::MCPServer::new(path);
                server.start()?;
                return Ok(());
            }
        }
        Command::Unlock { path } => {
            let root = resolve_root(path)?;
            let lock = root.join(".codegraph").join("codegraph.lock");
            if lock.exists() {
                std::fs::remove_file(lock)?;
            }
            println!("Unlocked");
        }
        Command::Install => {
            println!("Rust CodeGraph installer is not implemented yet. Run `cgz init -i` in a project.");
        }
    }
    Ok(())
}

fn resolve_root(path: Option<PathBuf>) -> Result<PathBuf> {
    let start = path.unwrap_or(std::env::current_dir()?);
    find_nearest_codegraph_root(&start)
        .ok_or_else(|| anyhow!("CodeGraph not initialized in {}", start.display()))
}

fn print_index_result(result: &codegraph::types::IndexResult) {
    println!(
        "Indexed {} files, {} nodes, {} edges in {}ms",
        result.files_indexed, result.nodes_created, result.edges_created, result.duration_ms
    );
    if !result.errors.is_empty() {
        eprintln!("Errors:");
        for err in &result.errors {
            eprintln!("  {err}");
        }
    }
}

fn is_test_file(file: &str) -> bool {
    file.contains("/__tests__/")
        || file.contains("/test/")
        || file.contains("/tests/")
        || file.contains("/e2e/")
        || file.contains("/spec/")
        || file.contains(".test.")
        || file.contains(".spec.")
}