codebase-graph 1.5.1

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use crate::adapters::{
    cli::{format::mcp_help, run},
    mcp::{serve_mcp_http, serve_mcp_stdio, McpHttpOptions, McpServeOptions},
};
use crate::daemon_service::{serve_mcp_daemon, McpDaemonOptions};
use std::{
    env,
    io::{self},
    path::Path,
};

pub fn run_from_env() -> Result<(), String> {
    if let Ok(executable) = env::current_exe() {
        crate::db_writer::register_phase_worker_executable(executable.clone());
        crate::materialization_worker::register_worker_executable(executable);
    }
    let args: Vec<String> = env::args().skip(1).collect();
    run_process_args(args)
}

pub(crate) fn run_process_args(args: Vec<String>) -> Result<(), String> {
    if args.first().map(String::as_str) == Some("__codebase_graph_internal") {
        return run_internal_command(&args[1..]);
    }
    if args.is_empty() {
        return run(args, &mut io::stdout());
    }
    if args.first().map(String::as_str) == Some("mcp") {
        match args.get(1).map(String::as_str) {
            Some("start") => {
                let options = McpServeOptions::parse(&args[2..], mcp_help())?;
                return serve_mcp_stdio(&options, io::stdin().lock(), &mut io::stdout());
            }
            Some("http") => {
                let options = McpHttpOptions::parse(&args[2..], mcp_help())?;
                return serve_mcp_http(&options);
            }
            Some("daemon") if args.get(2).map(String::as_str) == Some("serve") => {
                let options = McpDaemonOptions::parse(&args[3..])?;
                return serve_mcp_daemon(&options);
            }
            _ => {}
        }
    }
    run(args, &mut io::stdout())
}

fn run_internal_command(args: &[String]) -> Result<(), String> {
    match args.first().map(String::as_str) {
        Some("ladybug-write-phase-v1") => {
            let request_path = args
                .get(1)
                .ok_or_else(|| "Ladybug write phase requires a request path".to_string())?;
            if args.len() != 2 {
                return Err("Ladybug write phase accepts exactly one request path".to_string());
            }
            crate::db_writer::execute_phase_file(Path::new(request_path))
        }
        Some("materialization-worker-v1") => {
            let request_path = args.get(1).ok_or_else(|| {
                "materialization worker requires request and result paths".to_string()
            })?;
            let result_path = args.get(2).ok_or_else(|| {
                "materialization worker requires request and result paths".to_string()
            })?;
            if args.len() != 3 {
                return Err(
                    "materialization worker accepts exactly request and result paths".to_string(),
                );
            }
            crate::materialization_worker::execute_worker_file(
                Path::new(request_path),
                Path::new(result_path),
            )
        }
        Some(command) => Err(format!("unknown internal command: {command}")),
        None => Err("missing internal command".to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal_materialization_worker_mode_is_selected_before_public_commands() {
        let error = run_process_args(vec![
            "__codebase_graph_internal".to_string(),
            "materialization-worker-v1".to_string(),
        ])
        .unwrap_err();
        assert_eq!(
            error,
            "materialization worker requires request and result paths"
        );
    }
}