use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; pub mod manager; pub mod profile; pub use profile::{ GitProfile, TokenConfig, TokenType, UsageStats, ProfileComparison }; /// Application configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { /// Configuration version for migration #[serde(default = "default_version")] pub version: String, /// Default profile name pub default_profile: Option, /// All configured profiles #[serde(default)] pub profiles: HashMap, /// LLM configuration #[serde(default)] pub llm: LlmConfig, /// Commit configuration #[serde(default)] pub commit: CommitConfig, /// Tag configuration #[serde(default)] pub tag: TagConfig, /// Changelog configuration #[serde(default)] pub changelog: ChangelogConfig, /// Repository-specific profile mappings #[serde(default)] pub repo_profiles: HashMap, /// Whether to encrypt sensitive data #[serde(default = "default_true")] pub encrypt_sensitive: bool, /// Theme settings #[serde(default)] pub theme: ThemeConfig, /// Language settings #[serde(default)] pub language: LanguageConfig, } impl Default for AppConfig { fn default() -> Self { Self { version: default_version(), default_profile: None, profiles: HashMap::new(), llm: LlmConfig::default(), commit: CommitConfig::default(), tag: TagConfig::default(), changelog: ChangelogConfig::default(), repo_profiles: HashMap::new(), encrypt_sensitive: true, theme: ThemeConfig::default(), language: LanguageConfig::default(), } } } /// LLM configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LlmConfig { /// Default LLM provider #[serde(default = "default_llm_provider")] pub provider: String, /// OpenAI configuration #[serde(default)] pub openai: OpenAiConfig, /// Ollama configuration #[serde(default)] pub ollama: OllamaConfig, /// Anthropic Claude configuration #[serde(default)] pub anthropic: AnthropicConfig, /// Kimi (Moonshot AI) configuration #[serde(default)] pub kimi: KimiConfig, /// DeepSeek configuration #[serde(default)] pub deepseek: DeepSeekConfig, /// OpenRouter configuration #[serde(default)] pub openrouter: OpenRouterConfig, /// Custom API configuration #[serde(default)] pub custom: Option, /// Maximum tokens for generation #[serde(default = "default_max_tokens")] pub max_tokens: u32, /// Temperature for generation #[serde(default = "default_temperature")] pub temperature: f32, /// Timeout in seconds #[serde(default = "default_timeout")] pub timeout: u64, } impl Default for LlmConfig { fn default() -> Self { Self { provider: default_llm_provider(), openai: OpenAiConfig::default(), ollama: OllamaConfig::default(), anthropic: AnthropicConfig::default(), kimi: KimiConfig::default(), deepseek: DeepSeekConfig::default(), openrouter: OpenRouterConfig::default(), custom: None, max_tokens: default_max_tokens(), temperature: default_temperature(), timeout: default_timeout(), } } } /// OpenAI API configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAiConfig { /// API key pub api_key: Option, /// Model to use #[serde(default = "default_openai_model")] pub model: String, /// API base URL (for custom endpoints) #[serde(default = "default_openai_base_url")] pub base_url: String, } impl Default for OpenAiConfig { fn default() -> Self { Self { api_key: None, model: default_openai_model(), base_url: default_openai_base_url(), } } } /// Ollama configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OllamaConfig { /// Ollama server URL #[serde(default = "default_ollama_url")] pub url: String, /// Model to use #[serde(default = "default_ollama_model")] pub model: String, } impl Default for OllamaConfig { fn default() -> Self { Self { url: default_ollama_url(), model: default_ollama_model(), } } } /// Anthropic Claude configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnthropicConfig { /// API key pub api_key: Option, /// Model to use #[serde(default = "default_anthropic_model")] pub model: String, } impl Default for AnthropicConfig { fn default() -> Self { Self { api_key: None, model: default_anthropic_model(), } } } /// Kimi (Moonshot AI) configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KimiConfig { /// API key pub api_key: Option, /// Model to use #[serde(default = "default_kimi_model")] pub model: String, /// API base URL (for custom endpoints) #[serde(default = "default_kimi_base_url")] pub base_url: String, } impl Default for KimiConfig { fn default() -> Self { Self { api_key: None, model: default_kimi_model(), base_url: default_kimi_base_url(), } } } /// DeepSeek configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeepSeekConfig { /// API key pub api_key: Option, /// Model to use #[serde(default = "default_deepseek_model")] pub model: String, /// API base URL (for custom endpoints) #[serde(default = "default_deepseek_base_url")] pub base_url: String, } impl Default for DeepSeekConfig { fn default() -> Self { Self { api_key: None, model: default_deepseek_model(), base_url: default_deepseek_base_url(), } } } /// OpenRouter configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenRouterConfig { /// API key pub api_key: Option, /// Model to use #[serde(default = "default_openrouter_model")] pub model: String, /// API base URL (for custom endpoints) #[serde(default = "default_openrouter_base_url")] pub base_url: String, } impl Default for OpenRouterConfig { fn default() -> Self { Self { api_key: None, model: default_openrouter_model(), base_url: default_openrouter_base_url(), } } } /// Custom LLM API configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomLlmConfig { /// API endpoint URL pub url: String, /// API key (optional) pub api_key: Option, /// Model name pub model: String, /// Request format template (JSON) pub request_template: String, /// Response path to extract content (e.g., "choices.0.message.content") pub response_path: String, } /// Commit configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommitConfig { /// Default commit format #[serde(default = "default_commit_format")] pub format: CommitFormat, /// Enable AI generation by default #[serde(default = "default_true")] pub auto_generate: bool, /// Allow empty commits #[serde(default)] pub allow_empty: bool, /// Sign commits with GPG #[serde(default)] pub gpg_sign: bool, /// Default scope (optional) pub default_scope: Option, /// Maximum subject length #[serde(default = "default_max_subject_length")] pub max_subject_length: usize, /// Require scope #[serde(default)] pub require_scope: bool, /// Require body for certain types #[serde(default)] pub require_body: bool, /// Types that require body #[serde(default = "default_body_required_types")] pub body_required_types: Vec, } impl Default for CommitConfig { fn default() -> Self { Self { format: default_commit_format(), auto_generate: true, allow_empty: false, gpg_sign: false, default_scope: None, max_subject_length: default_max_subject_length(), require_scope: false, require_body: false, body_required_types: default_body_required_types(), } } } /// Commit format #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum CommitFormat { Conventional, Commitlint, } impl std::fmt::Display for CommitFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CommitFormat::Conventional => write!(f, "conventional"), CommitFormat::Commitlint => write!(f, "commitlint"), } } } /// Tag configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TagConfig { /// Default version prefix (e.g., "v") #[serde(default = "default_version_prefix")] pub version_prefix: String, /// Enable AI generation for tag messages #[serde(default = "default_true")] pub auto_generate: bool, /// Sign tags with GPG #[serde(default)] pub gpg_sign: bool, /// Include changelog in annotated tags #[serde(default = "default_true")] pub include_changelog: bool, /// Default annotation template #[serde(default)] pub annotation_template: Option, } impl Default for TagConfig { fn default() -> Self { Self { version_prefix: default_version_prefix(), auto_generate: true, gpg_sign: false, include_changelog: true, annotation_template: None, } } } /// Changelog configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChangelogConfig { /// Changelog file path #[serde(default = "default_changelog_path")] pub path: String, /// Enable AI generation for changelog entries #[serde(default = "default_true")] pub auto_generate: bool, /// Changelog format #[serde(default = "default_changelog_format")] pub format: ChangelogFormat, /// Include commit hashes #[serde(default)] pub include_hashes: bool, /// Include authors #[serde(default)] pub include_authors: bool, /// Group by type #[serde(default = "default_true")] pub group_by_type: bool, /// Custom categories #[serde(default)] pub custom_categories: Vec, } impl Default for ChangelogConfig { fn default() -> Self { Self { path: default_changelog_path(), auto_generate: true, format: default_changelog_format(), include_hashes: false, include_authors: false, group_by_type: true, custom_categories: vec![], } } } /// Changelog format #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum ChangelogFormat { KeepAChangelog, GitHubReleases, Custom, } /// Changelog category mapping #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChangelogCategory { /// Category title pub title: String, /// Commit types included in this category pub types: Vec, } /// Theme configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeConfig { /// Enable colors #[serde(default = "default_true")] pub colors: bool, /// Enable icons #[serde(default = "default_true")] pub icons: bool, /// Preferred date format #[serde(default = "default_date_format")] pub date_format: String, } impl Default for ThemeConfig { fn default() -> Self { Self { colors: true, icons: true, date_format: default_date_format(), } } } /// Language configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LanguageConfig { /// Output language for messages (en, zh, etc.) #[serde(default = "default_output_language")] pub output_language: String, /// Keep commit types in English #[serde(default = "default_true")] pub keep_types_english: bool, /// Keep changelog types in English #[serde(default = "default_true")] pub keep_changelog_types_english: bool, } impl Default for LanguageConfig { fn default() -> Self { Self { output_language: default_output_language(), keep_types_english: true, keep_changelog_types_english: true, } } } /// Supported languages #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { English, Chinese, Japanese, Korean, Spanish, French, German, } impl Language { pub fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "en" | "english" => Some(Language::English), "zh" | "chinese" | "zh-cn" | "zh-tw" => Some(Language::Chinese), "ja" | "japanese" => Some(Language::Japanese), "ko" | "korean" => Some(Language::Korean), "es" | "spanish" => Some(Language::Spanish), "fr" | "french" => Some(Language::French), "de" | "german" => Some(Language::German), _ => None, } } pub fn to_code(&self) -> &str { match self { Language::English => "en", Language::Chinese => "zh", Language::Japanese => "ja", Language::Korean => "ko", Language::Spanish => "es", Language::French => "fr", Language::German => "de", } } pub fn display_name(&self) -> &str { match self { Language::English => "English", Language::Chinese => "中文", Language::Japanese => "日本語", Language::Korean => "한국어", Language::Spanish => "Español", Language::French => "Français", Language::German => "Deutsch", } } } // Default value functions fn default_version() -> String { "1".to_string() } fn default_true() -> bool { true } fn default_llm_provider() -> String { "ollama".to_string() } fn default_max_tokens() -> u32 { 500 } fn default_temperature() -> f32 { 0.6 } fn default_timeout() -> u64 { 30 } fn default_openai_model() -> String { "gpt-4".to_string() } fn default_openai_base_url() -> String { "https://api.openai.com/v1".to_string() } fn default_ollama_url() -> String { "http://localhost:11434".to_string() } fn default_ollama_model() -> String { "llama2".to_string() } fn default_anthropic_model() -> String { "claude-3-sonnet-20240229".to_string() } fn default_kimi_model() -> String { "moonshot-v1-8k".to_string() } fn default_kimi_base_url() -> String { "https://api.moonshot.cn/v1".to_string() } fn default_deepseek_model() -> String { "deepseek-chat".to_string() } fn default_deepseek_base_url() -> String { "https://api.deepseek.com/v1".to_string() } fn default_openrouter_model() -> String { "openai/gpt-3.5-turbo".to_string() } fn default_openrouter_base_url() -> String { "https://openrouter.ai/api/v1".to_string() } fn default_commit_format() -> CommitFormat { CommitFormat::Conventional } fn default_max_subject_length() -> usize { 100 } fn default_body_required_types() -> Vec { vec!["feat".to_string(), "fix".to_string()] } fn default_version_prefix() -> String { "v".to_string() } fn default_changelog_path() -> String { "CHANGELOG.md".to_string() } fn default_changelog_format() -> ChangelogFormat { ChangelogFormat::KeepAChangelog } fn default_date_format() -> String { "%Y-%m-%d".to_string() } fn default_output_language() -> String { "en".to_string() } impl AppConfig { /// Load configuration from file pub fn load(path: &Path) -> Result { if path.exists() { let content = fs::read_to_string(path) .with_context(|| format!("Failed to read config file: {:?}", path))?; let config: AppConfig = toml::from_str(&content) .with_context(|| format!("Failed to parse config file: {:?}", path))?; Ok(config) } else { Ok(Self::default()) } } /// Save configuration to file pub fn save(&self, path: &Path) -> Result<()> { let content = toml::to_string_pretty(self) .context("Failed to serialize config")?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("Failed to create config directory: {:?}", parent))?; } fs::write(path, content) .with_context(|| format!("Failed to write config file: {:?}", path))?; Ok(()) } /// Get default config path pub fn default_path() -> Result { let config_dir = dirs::config_dir() .context("Could not find config directory")?; Ok(config_dir.join("quicommit").join("config.toml")) } /// Get profile for a repository pub fn get_profile_for_repo(&self, repo_path: &str) -> Option<&GitProfile> { let profile_name = self.repo_profiles.get(repo_path)?; self.profiles.get(profile_name) } /// Set profile for a repository pub fn set_profile_for_repo(&mut self, repo_path: String, profile_name: String) -> Result<()> { if !self.profiles.contains_key(&profile_name) { anyhow::bail!("Profile '{}' does not exist", profile_name); } self.repo_profiles.insert(repo_path, profile_name); Ok(()) } }