Files
QuiCommit/src/commands/config.rs
SidneyZhang 14ebb6857a feat(commit): 添加提交消息模板支持
- 移除 config 命令中未使用的 List 子命令及相关显示字段
- 统一 ChangelogCommand 和 CommitCommand 的 ContentGenerator 初始化方式
2026-06-03 15:20:50 +08:00

1210 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use colored::Colorize;
use dialoguer::{Confirm, Input, Password, Select};
use std::path::PathBuf;
use crate::config::CommitFormat;
use crate::config::{EncryptedPat, ExportData, Language, manager::ConfigManager};
use crate::utils::crypto::{decrypt, encrypt};
use crate::utils::keyring::{
get_default_base_url, get_default_model, get_supported_providers, provider_needs_api_key,
};
/// Mask API key with asterisks for security
fn mask_api_key(key: Option<&str>) -> String {
match key {
Some(k) => {
if k.len() <= 8 {
"*".repeat(k.len())
} else {
format!("{}***{}", &k[..4], &k[k.len() - 4..])
}
}
None => "✗ not set".red().to_string(),
}
}
/// Manage configuration settings
#[derive(Parser)]
pub struct ConfigCommand {
#[command(subcommand)]
command: Option<ConfigSubcommand>,
}
#[derive(Subcommand)]
enum ConfigSubcommand {
/// Show current configuration
Show,
/// Edit configuration file
Edit,
/// Set configuration value
Set {
/// Key (e.g., llm.provider, commit.format)
key: String,
/// Value
value: String,
},
/// Get configuration value
Get {
/// Key
key: String,
},
/// Configure LLM provider (interactive)
SetLlm {
/// Provider (ollama, openai, anthropic, kimi, deepseek, openrouter)
#[arg(value_name = "PROVIDER")]
provider: Option<String>,
/// Model name
#[arg(short, long)]
model: Option<String>,
/// API base URL (optional)
#[arg(short, long)]
base_url: Option<String>,
/// API key (will be stored in system keyring)
#[arg(short = 'k', long)]
api_key: Option<String>,
},
/// Set API key for current provider (stored in system keyring)
SetApiKey {
/// API key
key: String,
},
/// Delete API key from system keyring
DeleteApiKey,
/// Set commit format
SetCommitFormat {
/// Format (conventional, commitlint)
format: String,
},
/// Set version prefix for tags
SetVersionPrefix {
/// Prefix (e.g., 'v')
prefix: String,
},
/// Set changelog path
SetChangelogPath {
/// Path
path: String,
},
/// Set output language
SetLanguage {
/// Language code (en, zh, ja, ko, es, fr, de)
language: Option<String>,
},
/// Set whether to keep commit types in English
SetKeepTypesEnglish {
/// Keep types in English (true/false)
keep: bool,
},
/// Set whether to keep changelog types in English
SetKeepChangelogTypesEnglish {
/// Keep types in English (true/false)
keep: bool,
},
/// Reset configuration to defaults
Reset {
/// Skip confirmation
#[arg(short, long)]
force: bool,
},
/// Export configuration
Export {
/// Output file (defaults to stdout)
#[arg(short, long)]
output: Option<String>,
/// Password for encryption (will prompt if not provided)
#[arg(short = 'p', long)]
password: Option<String>,
},
/// Import configuration
Import {
/// Input file
#[arg(short, long)]
file: String,
/// Password for decryption (will prompt if file is encrypted)
#[arg(short = 'p', long)]
password: Option<String>,
},
/// List available LLM models
ListModels,
/// Test LLM connection
TestLlm,
/// Show config file path
Path,
/// Check keyring availability
CheckKeyring,
}
impl ConfigCommand {
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
match &self.command {
Some(ConfigSubcommand::Show) => self.show_config(&config_path).await,
Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await,
Some(ConfigSubcommand::Set { key, value }) => {
self.set_value(key, value, &config_path).await
}
Some(ConfigSubcommand::Get { key }) => self.get_value(key, &config_path).await,
Some(ConfigSubcommand::SetLlm {
provider,
model,
base_url,
api_key,
}) => {
self.set_llm(
provider.as_deref(),
model.as_deref(),
base_url.as_deref(),
api_key.as_deref(),
&config_path,
)
.await
}
Some(ConfigSubcommand::SetApiKey { key }) => self.set_api_key(key, &config_path).await,
Some(ConfigSubcommand::DeleteApiKey) => self.delete_api_key(&config_path).await,
Some(ConfigSubcommand::SetCommitFormat { format }) => {
self.set_commit_format(format, &config_path).await
}
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => {
self.set_version_prefix(prefix, &config_path).await
}
Some(ConfigSubcommand::SetChangelogPath { path }) => {
self.set_changelog_path(path, &config_path).await
}
Some(ConfigSubcommand::SetLanguage { language }) => {
self.set_language(language.as_deref(), &config_path).await
}
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => {
self.set_keep_types_english(*keep, &config_path).await
}
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => {
self.set_keep_changelog_types_english(*keep, &config_path)
.await
}
Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await,
Some(ConfigSubcommand::Export { output, password }) => {
self.export_config(output.as_deref(), password.as_deref(), &config_path)
.await
}
Some(ConfigSubcommand::Import { file, password }) => {
self.import_config(file, password.as_deref(), &config_path)
.await
}
Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await,
Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await,
Some(ConfigSubcommand::Path) => self.show_path(&config_path).await,
Some(ConfigSubcommand::CheckKeyring) => self.check_keyring(&config_path).await,
None => self.show_config(&config_path).await,
}
}
fn get_manager(&self, config_path: &Option<PathBuf>) -> Result<ConfigManager> {
match config_path {
Some(path) => ConfigManager::with_path(path),
None => ConfigManager::new(),
}
}
async fn show_path(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
println!("{}", manager.path().display());
Ok(())
}
async fn check_keyring(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let keyring = manager.keyring();
println!("{}", "\nKeyring Status".bold());
println!("{}", "".repeat(40));
if keyring.is_available() {
println!("{} Keyring is available", "".green());
println!(" {}", keyring.get_status_message());
} else {
println!("{} Keyring is not available", "".red());
println!(" {}", keyring.get_status_message());
}
println!("\n{}", "Environment Variables:".bold());
if let Ok(key) = std::env::var("QUICOMMIT_API_KEY") {
println!(" QUICOMMIT_API_KEY: {}", mask_api_key(Some(&key)));
} else {
println!(" QUICOMMIT_API_KEY: {}", "not set".dimmed());
}
let provider = manager.llm_provider();
let provider_env = format!("QUICOMMIT_{}_API_KEY", provider.to_uppercase());
if let Ok(key) = std::env::var(&provider_env) {
println!(" {}: {}", provider_env, mask_api_key(Some(&key)));
} else {
println!(" {}: {}", provider_env, "not set".dimmed());
}
Ok(())
}
async fn show_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
println!("{}", "\nQuiCommit Configuration".bold());
println!("{}", "".repeat(60));
println!("\n{}", "General:".bold());
println!(" Config file: {}", manager.path().display());
println!(
" Default profile: {}",
config.default_profile.as_deref().unwrap_or("(none)").cyan()
);
println!(" Profiles: {}", config.profiles.len());
println!("\n{}", "LLM Configuration:".bold());
println!(" Provider: {}", config.llm.provider.cyan());
println!(" Model: {}", config.llm.model.cyan());
let base_url = manager.llm_base_url();
println!(" Base URL: {}", base_url);
let api_key = manager.get_api_key();
println!(" API key: {}", mask_api_key(api_key.as_deref()));
println!(" Max tokens: {}", config.llm.max_tokens);
println!(" Temperature: {}", config.llm.temperature);
println!(" Timeout: {}s", config.llm.timeout);
println!("\n{}", "Commit Configuration:".bold());
println!(" Format: {}", config.commit.format.to_string().cyan());
println!(
" Auto-generate: {}",
if config.commit.auto_generate {
"yes".green()
} else {
"no".red()
}
);
println!("\n{}", "Tag Configuration:".bold());
println!(" Version prefix: '{}'", config.tag.version_prefix);
println!(
" Auto-generate: {}",
if config.tag.auto_generate {
"yes".green()
} else {
"no".red()
}
);
println!("\n{}", "Language Configuration:".bold());
let language = manager.get_language().unwrap_or(Language::English);
println!(" Output language: {}", language.display_name().cyan());
println!(
" Keep commit types in English: {}",
if manager.keep_types_english() {
"yes".green()
} else {
"no".red()
}
);
println!(
" Keep changelog types in English: {}",
if manager.keep_changelog_types_english() {
"yes".green()
} else {
"no".red()
}
);
println!("\n{}", "Changelog Configuration:".bold());
println!(" Path: {}", config.changelog.path);
println!(
" Auto-generate: {}",
if config.changelog.auto_generate {
"yes".green()
} else {
"no".red()
}
);
println!("\n{}", "Security:".bold());
println!(" Repository mappings: {} mapping(s)", config.repo_profiles.len());
println!(
" Keyring: {}",
if manager.keyring().is_available() {
"available".green()
} else {
"unavailable".red()
}
);
Ok(())
}
async fn edit_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
crate::utils::editor::edit_file(manager.path())?;
println!("{} Configuration updated", "".green());
Ok(())
}
async fn set_value(&self, key: &str, value: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
match key {
"llm.provider" => manager.set_llm_provider(value.to_string()),
"llm.model" => manager.set_llm_model(value.to_string()),
"llm.base_url" => manager.set_llm_base_url(Some(value.to_string())),
"llm.max_tokens" => {
let tokens: u32 = value.parse()?;
manager.config_mut().llm.max_tokens = tokens;
}
"llm.temperature" => {
let temp: f32 = value.parse()?;
manager.config_mut().llm.temperature = temp;
}
"llm.timeout" => {
let timeout: u64 = value.parse()?;
manager.config_mut().llm.timeout = timeout;
}
"llm.thinking_enabled" => {
manager.config_mut().llm.thinking_enabled = value == "true";
}
"llm.thinking_budget_tokens" => {
let budget: u32 = value.parse()?;
manager.config_mut().llm.thinking_budget_tokens = Some(budget);
}
"llm.api_key_storage" => {
let valid_values = ["keyring", "config", "environment"];
if !valid_values.contains(&value) {
bail!("Invalid value: {}. Use: {}", value, valid_values.join(", "));
}
manager.config_mut().llm.api_key_storage = value.to_string();
}
"commit.format" => {
let format = match value {
"conventional" => CommitFormat::Conventional,
"commitlint" => CommitFormat::Commitlint,
_ => bail!("Invalid format: {}. Use: conventional, commitlint", value),
};
manager.set_commit_format(format);
}
"commit.auto_generate" => {
manager.set_auto_generate_commits(value == "true");
}
"tag.version_prefix" => manager.set_version_prefix(value.to_string()),
"tag.auto_generate" => {
manager.config_mut().tag.auto_generate = value == "true";
}
"changelog.path" => manager.set_changelog_path(value.to_string()),
"changelog.auto_generate" => {
manager.config_mut().changelog.auto_generate = value == "true";
}
"language.output_language" => {
manager.set_output_language(value.to_string());
}
"language.keep_types_english" => {
manager.set_keep_types_english(value == "true");
}
"language.keep_changelog_types_english" => {
manager.set_keep_changelog_types_english(value == "true");
}
_ => bail!("Unknown configuration key: {}", key),
}
manager.save()?;
println!("{} Set {} = {}", "".green(), key.cyan(), value);
Ok(())
}
async fn get_value(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
let value = match key {
"llm.provider" => config.llm.provider.clone(),
"llm.model" => config.llm.model.clone(),
"llm.base_url" => manager.llm_base_url(),
"llm.max_tokens" => config.llm.max_tokens.to_string(),
"llm.temperature" => config.llm.temperature.to_string(),
"llm.timeout" => config.llm.timeout.to_string(),
"llm.thinking_enabled" => config.llm.thinking_enabled.to_string(),
"llm.thinking_budget_tokens" => config
.llm
.thinking_budget_tokens
.map(|v| v.to_string())
.unwrap_or_else(|| "none".to_string()),
"llm.api_key_storage" => config.llm.api_key_storage.clone(),
"commit.format" => config.commit.format.to_string(),
"commit.auto_generate" => config.commit.auto_generate.to_string(),
"tag.version_prefix" => config.tag.version_prefix.clone(),
"tag.auto_generate" => config.tag.auto_generate.to_string(),
"changelog.path" => config.changelog.path.clone(),
"changelog.auto_generate" => config.changelog.auto_generate.to_string(),
"language.output_language" => config.language.output_language.clone(),
"language.keep_types_english" => config.language.keep_types_english.to_string(),
"language.keep_changelog_types_english" => {
config.language.keep_changelog_types_english.to_string()
}
_ => bail!("Unknown configuration key: {}", key),
};
println!("{}", value);
Ok(())
}
async fn set_llm(
&self,
provider: Option<&str>,
model: Option<&str>,
base_url: Option<&str>,
api_key: Option<&str>,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let selected_provider = if let Some(p) = provider {
let providers = get_supported_providers();
if !providers.contains(&p) {
bail!(
"Invalid provider: {}. Valid options: {}",
p,
providers.join(", ")
);
}
p.to_string()
} else {
println!("{}", "Select LLM Provider:".bold());
let provider_display_names = vec![
"Ollama (local)",
"OpenAI",
"Anthropic Claude",
"Kimi (Moonshot AI)",
"DeepSeek",
"OpenRouter",
];
let provider_idx = Select::new()
.items(&provider_display_names)
.default(0)
.interact()?;
let providers = get_supported_providers();
providers[provider_idx].to_string()
};
let keyring = manager.keyring();
let keyring_available = keyring.is_available();
if !keyring_available && provider_needs_api_key(&selected_provider) {
println!(
"\n{}",
"⚠ Keyring is not available on this system.".yellow()
);
println!("{}", keyring.get_status_message().yellow());
}
let selected_model = if let Some(m) = model {
m.to_string()
} else {
let default_model = get_default_model(&selected_provider);
Input::new()
.with_prompt("Model name")
.default(default_model.to_string())
.interact_text()?
};
let selected_base_url = if let Some(u) = base_url {
Some(u.to_string())
} else if selected_provider == "ollama" {
let url: String = Input::new()
.with_prompt("Ollama server URL")
.default("http://localhost:11434".to_string())
.interact_text()?;
Some(url)
} else {
let use_custom = Confirm::new()
.with_prompt("Use custom API base URL?")
.default(false)
.interact()?;
if use_custom {
let default_url = get_default_base_url(&selected_provider);
let url: String = Input::new()
.with_prompt("Base URL")
.default(default_url.to_string())
.interact_text()?;
Some(url)
} else {
None
}
};
let selected_api_key = if provider_needs_api_key(&selected_provider) {
if let Some(k) = api_key {
Some(k.to_string())
} else if keyring_available {
let existing_key = manager.get_api_key();
if existing_key.is_some() {
let overwrite = Confirm::new()
.with_prompt("API key already exists. Update it?")
.default(false)
.interact()?;
if overwrite {
let key: String = Input::new().with_prompt("API key").interact_text()?;
Some(key)
} else {
None
}
} else {
let key: String = Input::new().with_prompt("API key").interact_text()?;
Some(key)
}
} else {
println!(
"\n{}",
"Please set the QUICOMMIT_API_KEY environment variable.".yellow()
);
None
}
} else {
None
};
manager.set_llm_provider(selected_provider.clone());
manager.set_llm_model(selected_model);
manager.set_llm_base_url(selected_base_url);
if let Some(key) = selected_api_key {
manager.set_api_key(&key)?;
println!(
"\n{} API key stored securely in system keyring",
"".green()
);
}
manager.save()?;
println!("\n{} LLM configuration updated", "".green());
println!(" Provider: {}", manager.llm_provider().cyan());
println!(" Model: {}", manager.llm_model().cyan());
println!(" Base URL: {}", manager.llm_base_url());
Ok(())
}
async fn set_api_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let provider = manager.llm_provider().to_string();
if !provider_needs_api_key(&provider) {
println!("{} {} does not require an API key", "".blue(), provider);
return Ok(());
}
let storage_method = manager.config().llm.api_key_storage.to_string();
match storage_method.as_str() {
"keyring" => {
if !manager.keyring().is_available() {
bail!(
"Keyring is not available. Set QUICOMMIT_API_KEY environment variable instead or change api_key_storage to 'config'."
);
}
manager.set_api_key(key)?;
println!(
"{} API key stored securely in system keyring for {}",
"".green(),
provider.cyan()
);
}
"config" => {
// Store API key directly in config file
manager.config_mut().llm.api_key = Some(key.to_string());
manager.save()?;
println!(
"{} API key stored in configuration file for {}",
"".green(),
provider.cyan()
);
println!(
"{} Note: API key is stored in plain text. Consider using 'keyring' storage for better security.",
"".yellow()
);
}
"environment" => {
bail!(
"API key storage set to 'environment'. Please set QUICOMMIT_{}_API_KEY environment variable.",
provider.to_uppercase()
);
}
_ => {
bail!("Invalid API key storage method: {}", storage_method);
}
}
Ok(())
}
async fn delete_api_key(&self, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let provider = manager.llm_provider().to_string();
let storage_method = manager.config().llm.api_key_storage.to_string();
match storage_method.as_str() {
"keyring" => {
if !manager.keyring().is_available() {
bail!("Keyring is not available.");
}
manager.delete_api_key()?;
println!(
"{} API key deleted from system keyring for {}",
"".green(),
provider.cyan()
);
}
"config" => {
// Remove API key from config file
manager.config_mut().llm.api_key = None;
manager.save()?;
println!(
"{} API key deleted from configuration file for {}",
"".green(),
provider.cyan()
);
}
"environment" => {
println!(
"{} API key storage set to 'environment'. Please remove QUICOMMIT_{}_API_KEY environment variable manually.",
"".blue(),
provider.to_uppercase()
);
}
_ => {
bail!("Invalid API key storage method: {}", storage_method);
}
}
Ok(())
}
async fn set_commit_format(&self, format: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let format = match format {
"conventional" => CommitFormat::Conventional,
"commitlint" => CommitFormat::Commitlint,
_ => bail!("Invalid format: {}. Use: conventional, commitlint", format),
};
manager.set_commit_format(format);
manager.save()?;
println!(
"{} Commit format set to {}",
"".green(),
format.to_string().cyan()
);
Ok(())
}
async fn set_version_prefix(&self, prefix: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_version_prefix(prefix.to_string());
manager.save()?;
println!("{} Version prefix set to '{}'", "".green(), prefix.cyan());
Ok(())
}
async fn set_changelog_path(&self, path: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_changelog_path(path.to_string());
manager.save()?;
println!("{} Changelog path set to {}", "".green(), path.cyan());
Ok(())
}
async fn set_language(
&self,
language: Option<&str>,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let lang_code = if let Some(l) = language {
l.to_string()
} else {
println!("{}", "Select Output Language:".bold());
let languages = [
("en", "English"),
("zh", "中文"),
("ja", "日本語"),
("ko", "한국어"),
("es", "Español"),
("fr", "Français"),
("de", "Deutsch"),
];
let lang_names: Vec<&str> = languages.iter().map(|(_, n)| *n).collect();
let idx = Select::new().items(&lang_names).default(0).interact()?;
languages[idx].0.to_string()
};
manager.set_output_language(lang_code.clone());
manager.save()?;
println!(
"{} Output language set to {}",
"".green(),
lang_code.cyan()
);
Ok(())
}
async fn set_keep_types_english(
&self,
keep: bool,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_keep_types_english(keep);
manager.save()?;
println!(
"{} Keep commit types in English: {}",
"".green(),
keep.to_string().cyan()
);
Ok(())
}
async fn set_keep_changelog_types_english(
&self,
keep: bool,
config_path: &Option<PathBuf>,
) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_keep_changelog_types_english(keep);
manager.save()?;
println!(
"{} Keep changelog types in English: {}",
"".green(),
keep.to_string().cyan()
);
Ok(())
}
async fn reset(&self, force: bool, config_path: &Option<PathBuf>) -> Result<()> {
if !force {
let confirm = Confirm::new()
.with_prompt("Reset all configuration to defaults?")
.default(false)
.interact()?;
if !confirm {
println!("Reset cancelled.");
return Ok(());
}
}
let mut manager = self.get_manager(config_path)?;
manager.reset();
manager.save()?;
println!("{} Configuration reset to defaults", "".green());
Ok(())
}
async fn export_config(
&self,
output: Option<&str>,
password: Option<&str>,
config_path: &Option<PathBuf>,
) -> Result<()> {
let manager = self.get_manager(config_path)?;
let toml = manager.export()?;
let export_content = if let Some(_path) = output {
let pwd = if let Some(p) = password {
p.to_string()
} else {
let confirm = Confirm::new()
.with_prompt("Encrypt the exported configuration?")
.default(true)
.interact()?;
if confirm {
let pwd1 = Password::new()
.with_prompt("Enter encryption password")
.interact()?;
let pwd2 = Password::new()
.with_prompt("Confirm encryption password")
.interact()?;
if pwd1 != pwd2 {
bail!("Passwords do not match");
}
pwd1
} else {
String::new()
}
};
if pwd.is_empty() {
let mut has_pats = false;
for (profile_name, profile) in manager.config().profiles.iter() {
for service in profile.tokens.keys() {
if manager.has_pat_for_profile(profile_name, service) {
has_pats = true;
break;
}
}
}
if has_pats {
println!(
"{} {}",
"".yellow(),
"WARNING: Exporting without encryption.".bold()
);
println!(
" {}",
"Personal Access Tokens (PATs) stored in keyring will NOT be exported."
.yellow()
);
println!(
" {}",
"To export PATs securely, please enable encryption.".yellow()
);
println!();
}
toml
} else {
let mut encrypted_pats: Vec<EncryptedPat> = Vec::new();
for (profile_name, profile) in manager.config().profiles.iter() {
for service in profile.tokens.keys() {
if let Ok(Some(pat_value)) =
manager.get_pat_for_profile(profile_name, service)
{
let encrypted_token = encrypt(pat_value.as_bytes(), &pwd)?;
encrypted_pats.push(EncryptedPat {
profile_name: profile_name.clone(),
service: service.clone(),
user_email: profile.user_email.clone(),
encrypted_token,
});
}
}
}
let export_data = ExportData::with_encrypted_pats(toml, encrypted_pats);
let export_json = serde_json::to_string(&export_data)
.context("Failed to serialize export data")?;
let encrypted = encrypt(export_json.as_bytes(), &pwd)?;
format!("ENCRYPTED:{}", encrypted)
}
} else {
toml
};
match output {
Some(path) => {
std::fs::write(path, &export_content)?;
if export_content.starts_with("ENCRYPTED:") {
println!(
"{} Configuration encrypted and exported to {}",
"".green(),
path
);
} else {
println!("{} Configuration exported to {}", "".green(), path);
}
}
None => {
println!("{}", export_content);
}
}
Ok(())
}
async fn import_config(
&self,
file: &str,
password: Option<&str>,
config_path: &Option<PathBuf>,
) -> Result<()> {
let content = std::fs::read_to_string(file)?;
let (config_content, encrypted_pats, pwd) = if content.starts_with("ENCRYPTED:") {
let encrypted_data = content.strip_prefix("ENCRYPTED:").unwrap();
let pwd = if let Some(p) = password {
p.to_string()
} else {
Password::new()
.with_prompt("Enter decryption password")
.interact()?
};
let decrypted = match decrypt(encrypted_data, &pwd) {
Ok(d) => d,
Err(e) => {
bail!(
"Failed to decrypt configuration: {}. Please check your password.",
e
);
}
};
let decrypted_str = String::from_utf8(decrypted)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?;
match serde_json::from_str::<ExportData>(&decrypted_str) {
Ok(export_data) => (
export_data.config,
Some(export_data.encrypted_pats),
Some(pwd),
),
Err(_) => (decrypted_str, None, Some(pwd)),
}
} else {
(content, None, None)
};
let mut manager = self.get_manager(config_path)?;
manager.import(&config_content)?;
manager.save()?;
if let (Some(pats), Some(pwd)) = (encrypted_pats, pwd)
&& !pats.is_empty()
{
println!();
println!("{}", "Importing Personal Access Tokens...".bold());
let mut imported_count = 0;
let mut failed_count = 0;
for pat in pats {
match decrypt(&pat.encrypted_token, &pwd) {
Ok(token_bytes) => match String::from_utf8(token_bytes) {
Ok(token_value) => {
if manager.keyring().is_available() {
match manager.store_pat_for_profile(
&pat.profile_name,
&pat.service,
&token_value,
) {
Ok(_) => {
println!(
" {} Token for {} ({}) imported to keyring",
"".green(),
pat.profile_name.cyan(),
pat.service.yellow()
);
imported_count += 1;
}
Err(e) => {
println!(
" {} Failed to store token for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e
);
failed_count += 1;
}
}
} else {
println!(
" {} Keyring not available, cannot store token for {} ({})",
"".yellow(),
pat.profile_name,
pat.service
);
failed_count += 1;
}
}
Err(e) => {
println!(
" {} Invalid token format for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e
);
failed_count += 1;
}
},
Err(e) => {
println!(
" {} Failed to decrypt token for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e
);
failed_count += 1;
}
}
}
println!();
if imported_count > 0 {
println!(
"{} {} token(s) imported to keyring",
"".green(),
imported_count
);
}
if failed_count > 0 {
println!(
"{} {} token(s) failed to import",
"".yellow(),
failed_count
);
}
}
println!("{} Configuration imported from {}", "".green(), file);
Ok(())
}
async fn list_models(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let provider = manager.llm_provider();
println!("{}", "\nAvailable Models".bold());
println!("{}", "".repeat(40));
match provider {
"ollama" => {
println!("Ollama models (local):");
println!(" llama2, llama2-uncensored, llama2:13b");
println!(" codellama, codellama:34b");
println!(" mistral, mixtral, phi, gemma");
println!("\nRun 'ollama list' to see installed models");
}
"openai" => {
println!("OpenAI models:");
println!(" o-series (reasoning): o4-mini, o3, o3-mini, o1, o1-mini, o1-pro");
println!(" GPT-4: gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-4o, gpt-4o-mini");
println!(" Legacy: gpt-4-turbo, gpt-4, gpt-3.5-turbo");
println!("\nUse --think/-t with o-series models for reasoning mode.");
}
"anthropic" => {
println!("Anthropic Claude models:");
println!(
" Claude 4 (thinking): claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5"
);
println!(
" Claude 3.5: claude-3-opus-20240229, claude-3-sonnet-20240229, claude-3-haiku-20240307"
);
println!(" Legacy: claude-2.1, claude-2.0, claude-instant-1.2");
println!("\nUse --think/-t with Claude 4 models for extended thinking.");
}
"kimi" => {
println!("Kimi (Moonshot AI) models:");
println!(
" K2 (thinking): kimi-k2.6, kimi-k2.5, kimi-k2-thinking, kimi-k2-thinking-turbo"
);
println!(" K2 instruct: kimi-k2-instruct, kimi-k2-instruct-0905");
println!(" Legacy: moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k");
println!("\nUse --think/-t with K2 models for thinking mode.");
}
"deepseek" => {
println!("DeepSeek models:");
println!(" V4: deepseek-v4-flash, deepseek-v4-pro");
println!(" Legacy: deepseek-chat, deepseek-reasoner (deprecated 2026-07-24)");
println!("\nUse --think/-t for reasoning mode.");
}
"openrouter" => {
println!("OpenRouter models (examples):");
println!(" openai/gpt-4, openai/gpt-3.5-turbo");
println!(" anthropic/claude-3-opus");
println!(" google/gemini-pro");
println!(" meta-llama/llama-2-70b-chat");
println!("\nSee https://openrouter.ai/models for full list");
}
_ => {
println!("Unknown provider: {}", provider);
}
}
println!("\nCurrent model: {}", manager.llm_model().cyan());
Ok(())
}
async fn test_llm(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
println!("{}", "\nTesting LLM Connection...".bold());
println!(" Provider: {}", manager.llm_provider().cyan());
println!(" Model: {}", manager.llm_model().cyan());
println!(" Base URL: {}", manager.llm_base_url());
let has_key = manager.has_api_key();
if provider_needs_api_key(manager.llm_provider()) {
println!(
" API Key: {}",
if has_key {
"✓ configured".green()
} else {
"✗ not set".red()
}
);
}
println!("\n{}", "Sending test request...".dimmed());
match crate::llm::test_connection(&manager).await {
Ok(response) => {
println!("{} Connection successful!", "".green());
println!("\n{}", "Response:".bold());
println!(" {}", response);
}
Err(e) => {
println!("{} Connection failed: {}", "".red(), e);
return Err(e);
}
}
Ok(())
}
}