gigi-cli 1.0.0

Gigi — A Claude Code-like AI coding assistant CLI in Rust
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConfigStore {
    pub anthropic_api_key: Option<String>,
    pub groq_api_key: Option<String>,
    pub google_api_key: Option<String>,
    pub custom_api_key: Option<String>,
    pub default_provider: Option<String>,
    pub default_model: Option<String>,
    pub ollama_url: Option<String>,
    pub lm_studio_url: Option<String>,
    pub llama_cpp_url: Option<String>,
    pub custom_api_url: Option<String>,
    pub tech_query_url: Option<String>,
}

impl ConfigStore {
    /// Get the path to the persistent config file: ~/.gigi_config.json
    pub fn config_path() -> PathBuf {
        let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        path.push(".gigi_config.json");
        path
    }

    /// Check if the configuration file exists.
    pub fn exists() -> bool {
        Self::config_path().exists()
    }

    /// Load the configuration from ~/.gigi_config.json
    pub fn load() -> Self {
        let path = Self::config_path();
        if path.exists() {
            if let Ok(json) = fs::read_to_string(&path) {
                if let Ok(store) = serde_json::from_str::<ConfigStore>(&json) {
                    return store;
                }
            }
        }
        ConfigStore::default()
    }

    /// Save the configuration to ~/.gigi_config.json
    pub fn save(&self) -> anyhow::Result<()> {
        let path = Self::config_path();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let json = serde_json::to_string_pretty(self)?;
        fs::write(&path, json)?;
        Ok(())
    }

    /// Runs an interactive CLI setup wizard to prompt the user for provider keys and preferences.
    pub fn run_setup_wizard() -> anyhow::Result<Self> {
        use std::io::{Write, stdin, stdout};
        use colored::*;

        println!("{}", "\n━━━ Gigi Setup Wizard ━━━".bold().cyan());
        println!("Welcome to Gigi! Let's get you set up with your AI provider first.\n");

        println!("Choose your default AI provider:");
        println!("  1) Anthropic Claude (Cloud API) [Recommended]");
        println!("  2) Google Gemini (Cloud API)");
        println!("  3) Groq (Cloud API)");
        println!("  4) Ollama (Local/Offline)");
        println!("  5) LM Studio (Local/Offline)");
        println!("  6) llama.cpp (Local/Offline)");
        println!("  7) Custom OpenAI-compatible Endpoint");

        let mut provider_choice = String::new();
        loop {
            print!("Select an option (1-7) [1]: ");
            let _ = stdout().flush();
            let mut input = String::new();
            stdin().read_line(&mut input)?;
            let trimmed = input.trim();
            if trimmed.is_empty() {
                provider_choice = "1".to_string();
                break;
            }
            if matches!(trimmed, "1" | "2" | "3" | "4" | "5" | "6" | "7") {
                provider_choice = trimmed.to_string();
                break;
            }
            println!("Invalid selection. Please enter a number between 1 and 7.");
        }

        let mut store = ConfigStore::default();
        
        match provider_choice.as_str() {
            "1" => {
                store.default_provider = Some("anthropic".to_string());
                println!("\nAnthropic Claude requires an API key (starts with sk-ant-).");
                print!("Enter your Anthropic API Key: ");
                let _ = stdout().flush();
                let mut key = String::new();
                stdin().read_line(&mut key)?;
                let trimmed_key = key.trim().to_string();
                if !trimmed_key.is_empty() {
                    store.anthropic_api_key = Some(trimmed_key);
                }
            }
            "2" => {
                store.default_provider = Some("google".to_string());
                println!("\nGoogle AI Studio (Gemini) requires an API key.");
                print!("Enter your Google API Key: ");
                let _ = stdout().flush();
                let mut key = String::new();
                stdin().read_line(&mut key)?;
                let trimmed_key = key.trim().to_string();
                if !trimmed_key.is_empty() {
                    store.google_api_key = Some(trimmed_key);
                }
            }
            "3" => {
                store.default_provider = Some("groq".to_string());
                println!("\nGroq requires an API key (starts with gsk_).");
                print!("Enter your Groq API Key: ");
                let _ = stdout().flush();
                let mut key = String::new();
                stdin().read_line(&mut key)?;
                let trimmed_key = key.trim().to_string();
                if !trimmed_key.is_empty() {
                    store.groq_api_key = Some(trimmed_key);
                }
            }
            "4" => {
                store.default_provider = Some("ollama".to_string());
                println!("\nOllama runs locally.");
                print!("Enter Ollama server URL [http://localhost:11434]: ");
                let _ = stdout().flush();
                let mut url = String::new();
                stdin().read_line(&mut url)?;
                let trimmed_url = url.trim().to_string();
                store.ollama_url = Some(if trimmed_url.is_empty() {
                    "http://localhost:11434".to_string()
                } else {
                    trimmed_url
                });
            }
            "5" => {
                store.default_provider = Some("lm_studio".to_string());
                println!("\nLM Studio runs locally.");
                print!("Enter LM Studio server URL [http://localhost:1234]: ");
                let _ = stdout().flush();
                let mut url = String::new();
                stdin().read_line(&mut url)?;
                let trimmed_url = url.trim().to_string();
                store.lm_studio_url = Some(if trimmed_url.is_empty() {
                    "http://localhost:1234".to_string()
                } else {
                    trimmed_url
                });
            }
            "6" => {
                store.default_provider = Some("llama_cpp".to_string());
                println!("\nllama.cpp server runs locally.");
                print!("Enter llama.cpp server URL [http://localhost:8080]: ");
                let _ = stdout().flush();
                let mut url = String::new();
                stdin().read_line(&mut url)?;
                let trimmed_url = url.trim().to_string();
                store.llama_cpp_url = Some(if trimmed_url.is_empty() {
                    "http://localhost:8080".to_string()
                } else {
                    trimmed_url
                });
            }
            "7" => {
                store.default_provider = Some("custom".to_string());
                print!("Enter custom endpoint URL [http://localhost:8000]: ");
                let _ = stdout().flush();
                let mut url = String::new();
                stdin().read_line(&mut url)?;
                let trimmed_url = url.trim().to_string();
                store.custom_api_url = Some(if trimmed_url.is_empty() {
                    "http://localhost:8000".to_string()
                } else {
                    trimmed_url
                });
                print!("Enter custom endpoint API key (optional): ");
                let _ = stdout().flush();
                let mut key = String::new();
                stdin().read_line(&mut key)?;
                let trimmed_key = key.trim().to_string();
                if !trimmed_key.is_empty() {
                    store.custom_api_key = Some(trimmed_key);
                }
            }
            _ => unreachable!(),
        }

        println!("\nWould you like to configure your custom PyTorch documentation search endpoint (tech_query)? (y/N): ");
        let _ = stdout().flush();
        let mut configure_tech = String::new();
        stdin().read_line(&mut configure_tech)?;
        let trimmed_tech = configure_tech.trim().to_lowercase();
        if trimmed_tech == "y" || trimmed_tech == "yes" {
            print!("Enter PyTorch search URL [http://localhost:5000/search]: ");
            let _ = stdout().flush();
            let mut url = String::new();
            stdin().read_line(&mut url)?;
            let trimmed_url = url.trim().to_string();
            store.tech_query_url = Some(if trimmed_url.is_empty() {
                "http://localhost:5000/search".to_string()
            } else {
                trimmed_url
            });
        }

        store.save()?;
        println!("{}", "\n✓ Setup complete! Configuration saved to ~/.gigi_config.json\n".green().bold());
        
        Ok(store)
    }
}