ragcli 0.2.0

CLI for local RAG
use crate::config::{
    self, ensure_store_layout, load_or_create_config_with_sources, load_or_create_file_config,
    save_config, store_dir, Config, ConfigSources,
};
use crate::ui::{self, Panel};
use anyhow::Result;
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct ConfigShowReport {
    pub store: String,
    pub config_path: String,
    pub config: Config,
    pub sources: ConfigSources,
    pub active_overrides: Vec<String>,
}

pub async fn show(name: Option<&str>, json: bool) -> Result<()> {
    let report = build_show_report(name)?;
    if json {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        print_show_human(&report)?;
    }
    Ok(())
}

fn build_show_report(name: Option<&str>) -> Result<ConfigShowReport> {
    let store = store_dir(name)?;
    ensure_store_layout(&store)?;
    let (cfg, sources) = load_or_create_config_with_sources(&store)?;
    let active_overrides = sources.overrides();

    Ok(ConfigShowReport {
        store: store.display().to_string(),
        config_path: config::config_path(&store).display().to_string(),
        config: cfg,
        sources,
        active_overrides,
    })
}

fn print_show_human(report: &ConfigShowReport) -> Result<()> {
    ui::command_header("ragcli config show", "");

    let mut location = Panel::new("Config Location");
    location.kv("store", &report.store, 6);
    location.kv("file", &report.config_path, 6);
    location.render();

    println!();
    let mut config = Panel::new("Config");
    for line in toml::to_string_pretty(&report.config)?.lines() {
        config.push(format!("  {line}"));
    }
    config.render();

    if !report.active_overrides.is_empty() {
        println!();
        let mut overrides = Panel::new("Active Environment Overrides");
        for override_entry in &report.active_overrides {
            overrides.prose("env", override_entry, 4);
        }
        overrides.render();
    }

    Ok(())
}

pub async fn set(name: Option<&str>, key: String, value: String) -> Result<()> {
    let store = store_dir(name)?;
    ensure_store_layout(&store)?;
    let mut cfg = load_or_create_file_config(&store)?;
    cfg.set_path(&key, &value)?;
    save_config(&store, &cfg)?;

    let mut panel = Panel::new("Config Updated");
    panel.kv("file", config::config_path(&store).display().to_string(), 6);
    panel.kv("key", key, 6);
    panel.kv("value", value, 6);
    panel.kv("status", ui::ok("saved"), 6);
    panel.render();
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::load_or_create_file_config;
    use crate::test_support::with_test_env;

    #[tokio::test(flavor = "current_thread")]
    async fn test_set_and_show_succeed_for_named_store() {
        let dir = tempfile::tempdir().unwrap();
        with_test_env(dir.path(), None, || async {
            set(
                Some("test-store"),
                "models.chat".to_string(),
                "chat-z".to_string(),
            )
            .await
            .unwrap();
            show(Some("test-store"), false).await.unwrap();

            let store = store_dir(Some("test-store")).unwrap();
            let cfg = load_or_create_file_config(&store).unwrap();
            assert_eq!(cfg.models.chat, "chat-z");
        })
        .await;
    }

    #[tokio::test(flavor = "current_thread")]
    async fn test_build_show_report_serializes_to_json() {
        let dir = tempfile::tempdir().unwrap();
        with_test_env(dir.path(), None, || async {
            let report = build_show_report(Some("json-store")).unwrap();
            let json = serde_json::to_string(&report).unwrap();
            assert!(json.contains("\"config\""));
            assert!(json.contains("\"active_overrides\""));
        })
        .await;
    }
}