chore: 升级版本至 0.1.10 并更新密钥环与加密相关描述
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use dialoguer::{Confirm, Input, Select, Password};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::{Language, manager::ConfigManager};
|
||||
use crate::config::CommitFormat;
|
||||
use crate::utils::keyring::{get_supported_providers, get_default_model, get_default_base_url, provider_needs_api_key};
|
||||
use crate::utils::crypto::{encrypt, decrypt};
|
||||
|
||||
/// Mask API key with asterisks for security
|
||||
fn mask_api_key(key: Option<&str>) -> String {
|
||||
@@ -130,6 +131,10 @@ enum ConfigSubcommand {
|
||||
/// 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
|
||||
@@ -137,6 +142,10 @@ enum ConfigSubcommand {
|
||||
/// 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
|
||||
@@ -172,8 +181,8 @@ impl ConfigCommand {
|
||||
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 }) => self.export_config(output.as_deref(), &config_path).await,
|
||||
Some(ConfigSubcommand::Import { file }) => self.import_config(file, &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,
|
||||
@@ -737,28 +746,90 @@ impl ConfigCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn export_config(&self, output: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||
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() {
|
||||
toml
|
||||
} else {
|
||||
let encrypted = encrypt(toml.as_bytes(), &pwd)?;
|
||||
format!("ENCRYPTED:{}", encrypted)
|
||||
}
|
||||
} else {
|
||||
toml
|
||||
};
|
||||
|
||||
match output {
|
||||
Some(path) => {
|
||||
std::fs::write(path, &toml)?;
|
||||
println!("{} Configuration exported to {}", "✓".green(), 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!("{}", toml);
|
||||
println!("{}", export_content);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_config(&self, file: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||||
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 = 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()?
|
||||
};
|
||||
|
||||
match decrypt(encrypted_data, &pwd) {
|
||||
Ok(decrypted) => String::from_utf8(decrypted)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?,
|
||||
Err(e) => {
|
||||
bail!("Failed to decrypt configuration: {}. Please check your password.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
let mut manager = self.get_manager(config_path)?;
|
||||
manager.import(&content)?;
|
||||
manager.import(&config_content)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Configuration imported from {}", "✓".green(), file);
|
||||
|
||||
Reference in New Issue
Block a user