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 {
pub fn config_path() -> PathBuf {
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
path.push(".gigi_config.json");
path
}
pub fn exists() -> bool {
Self::config_path().exists()
}
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()
}
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(())
}
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)
}
}