codebase-graph 1.2.1

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::format::uninstall_help;
use crate::adapters::required_arg;
use crate::api::{
    CodebaseGraphApi, OperationRequest, OutputFormat, RepoSelector, RepositoryLifecycleRequest,
};
use std::{io::Write, path::PathBuf};

#[derive(Debug)]
pub(in crate::adapters::cli) struct UninstallOptions {
    repo_root: Option<PathBuf>,
    config: Option<PathBuf>,
    mcp_client: String,
    client_config_path: Option<PathBuf>,
    dry_run: bool,
    json: bool,
    help: bool,
}

impl UninstallOptions {
    fn parse(args: &[String]) -> Result<Self, String> {
        let mut options = Self {
            repo_root: None,
            config: None,
            mcp_client: "all".to_string(),
            client_config_path: None,
            dry_run: false,
            json: false,
            help: false,
        };
        let mut index = 0;
        while index < args.len() {
            match args[index].as_str() {
                "-h" | "--help" => {
                    options.help = true;
                    index += 1;
                }
                "--repo-root" | "--source-root" => {
                    options.repo_root =
                        Some(PathBuf::from(required_arg(args, index, "--repo-root")?));
                    index += 2;
                }
                "--config" => {
                    options.config = Some(PathBuf::from(required_arg(args, index, "--config")?));
                    index += 2;
                }
                "--mcp-client" => {
                    let client = required_arg(args, index, "--mcp-client")?;
                    options.mcp_client = client.to_string();
                    index += 2;
                }
                "--client-config-path" => {
                    options.client_config_path = Some(PathBuf::from(required_arg(
                        args,
                        index,
                        "--client-config-path",
                    )?));
                    index += 2;
                }
                "--dry-run" => {
                    options.dry_run = true;
                    index += 1;
                }
                "--json" => {
                    options.json = true;
                    index += 1;
                }
                other => {
                    return Err(format!(
                        "unknown uninstall option: {other}\n\n{}",
                        uninstall_help()
                    ));
                }
            }
        }
        if options.client_config_path.is_some() && options.mcp_client == "all" {
            return Err("--client-config-path requires --mcp-client <client>".to_string());
        }
        Ok(options)
    }
}

pub(in crate::adapters::cli) fn run_uninstall<W: Write>(
    args: &[String],
    stdout: &mut W,
) -> Result<(), String> {
    let options = UninstallOptions::parse(args)?;
    if options.help {
        writeln!(stdout, "{}", uninstall_help()).map_err(|error| error.to_string())?;
        return Ok(());
    }
    let request = RepositoryLifecycleRequest {
        repo: RepoSelector {
            repo_root: options.repo_root.clone(),
            config_path: options.config.clone(),
            db_path: None,
            manifest_path: None,
        },
        action: "uninstall".to_string(),
        output_format: if options.json {
            OutputFormat::Typed
        } else {
            OutputFormat::Block
        },
        dry_run: options.dry_run,
        mcp_client: Some(options.mcp_client.clone()),
        mcp_config_path: options.client_config_path.clone(),
        instructions_target: None,
        skip_mcp_config: false,
        mode: "changed".to_string(),
        include_fts: true,
        semantic_enrichment: true,
        semantic_provider_mode: "local_only".to_string(),
    };
    let payload = CodebaseGraphApi::new()
        .execute_operation(&OperationRequest::Uninstall(request))
        .map_err(|error| error.message)?
        .payload;
    if options.json {
        writeln!(
            stdout,
            "{}",
            serde_json::to_string_pretty(&payload).map_err(|error| error.to_string())?
        )
        .map_err(|error| error.to_string())?;
    } else {
        let text = payload
            .get("text")
            .and_then(serde_json::Value::as_str)
            .ok_or_else(|| "block response did not contain text".to_string())?;
        write!(stdout, "{text}").map_err(|error| error.to_string())?;
    }
    Ok(())
}