feat(config): 在加密导出/导入中包含个人访问令牌
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use dialoguer::{Confirm, Input, Select, Password};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::{Language, manager::ConfigManager};
|
||||
use crate::config::{Language, manager::ConfigManager, ExportData, EncryptedPat};
|
||||
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};
|
||||
@@ -777,9 +777,45 @@ impl ConfigCommand {
|
||||
};
|
||||
|
||||
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 encrypted = encrypt(toml.as_bytes(), &pwd)?;
|
||||
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 {
|
||||
@@ -806,7 +842,7 @@ impl ConfigCommand {
|
||||
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 (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 {
|
||||
@@ -817,21 +853,106 @@ impl ConfigCommand {
|
||||
.interact()?
|
||||
};
|
||||
|
||||
match decrypt(encrypted_data, &pwd) {
|
||||
Ok(decrypted) => String::from_utf8(decrypted)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?,
|
||||
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
|
||||
(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) {
|
||||
if !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(())
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ impl ProfileCommand {
|
||||
.interact()?;
|
||||
|
||||
if setup_token {
|
||||
self.setup_token_interactive(&mut profile).await?;
|
||||
self.setup_token_interactive(&mut profile, &manager).await?;
|
||||
}
|
||||
|
||||
manager.add_profile(name.clone(), profile)?;
|
||||
@@ -269,10 +269,12 @@ impl ProfileCommand {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
manager.delete_all_pats_for_profile(name)?;
|
||||
|
||||
manager.remove_profile(name)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Profile '{}' removed", "✓".green(), name);
|
||||
println!("{} Profile '{}' removed (including all stored tokens)", "✓".green(), name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -330,16 +332,171 @@ impl ProfileCommand {
|
||||
async fn show_profile(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||
let manager = self.get_manager(config_path)?;
|
||||
|
||||
let profile = if let Some(n) = name {
|
||||
manager.get_profile(n)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?
|
||||
match find_repo(std::env::current_dir()?.as_path()) {
|
||||
Ok(repo) => {
|
||||
self.show_repo_status(&repo, &manager, name).await
|
||||
}
|
||||
Err(_) => {
|
||||
self.show_global_status(&manager, name).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn show_repo_status(&self, repo: &crate::git::GitRepo, manager: &ConfigManager, name: Option<&str>) -> Result<()> {
|
||||
use crate::git::MergedUserConfig;
|
||||
|
||||
let merged_config = MergedUserConfig::from_repo(repo.inner())?;
|
||||
let repo_path = repo.path().to_string_lossy().to_string();
|
||||
|
||||
println!("{}", "\n📁 Current Repository Status".bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("Repository: {}", repo_path.cyan());
|
||||
|
||||
println!("\n{}", "Git User Configuration (merged local/global):".bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
|
||||
self.print_config_entry("User name", &merged_config.name);
|
||||
self.print_config_entry("User email", &merged_config.email);
|
||||
self.print_config_entry("Signing key", &merged_config.signing_key);
|
||||
self.print_config_entry("SSH command", &merged_config.ssh_command);
|
||||
self.print_config_entry("Commit GPG sign", &merged_config.commit_gpgsign);
|
||||
self.print_config_entry("Tag GPG sign", &merged_config.tag_gpgsign);
|
||||
|
||||
let user_name = merged_config.name.value.clone().unwrap_or_default();
|
||||
let user_email = merged_config.email.value.clone().unwrap_or_default();
|
||||
let signing_key = merged_config.signing_key.value.as_deref();
|
||||
|
||||
let matching_profile = manager.find_matching_profile(&user_name, &user_email, signing_key);
|
||||
let repo_profile_name = manager.get_repo_profile_name(&repo_path);
|
||||
|
||||
println!("\n{}", "QuiCommit Profile Status:".bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
|
||||
match (&matching_profile, repo_profile_name) {
|
||||
(Some(profile), Some(mapped_name)) => {
|
||||
if profile.name == *mapped_name {
|
||||
println!("{} Profile '{}' is mapped to this repository", "✓".green(), profile.name.cyan());
|
||||
println!(" This repository's git config matches the saved profile.");
|
||||
} else {
|
||||
println!("{} Profile '{}' matches current config", "✓".green(), profile.name.cyan());
|
||||
println!(" But repository is mapped to different profile: {}", mapped_name.yellow());
|
||||
}
|
||||
}
|
||||
(Some(profile), None) => {
|
||||
println!("{} Profile '{}' matches current config", "✓".green(), profile.name.cyan());
|
||||
println!(" {} This repository is not mapped to any profile.", "ℹ".yellow());
|
||||
}
|
||||
(None, Some(mapped_name)) => {
|
||||
println!("{} Repository is mapped to profile '{}'", "⚠".yellow(), mapped_name.cyan());
|
||||
println!(" But current git config does not match this profile!");
|
||||
|
||||
if let Some(mapped_profile) = manager.get_profile(mapped_name) {
|
||||
println!("\n Mapped profile config:");
|
||||
println!(" user.name: {}", mapped_profile.user_name);
|
||||
println!(" user.email: {}", mapped_profile.user_email);
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
println!("{} No matching profile found in QuiCommit", "✗".red());
|
||||
println!(" Current git identity is not saved as a QuiCommit profile.");
|
||||
|
||||
let partial_matches = manager.find_partial_matches(&user_name, &user_email);
|
||||
if !partial_matches.is_empty() {
|
||||
println!("\n {} Similar profiles exist:", "ℹ".yellow());
|
||||
for p in partial_matches {
|
||||
let same_name = p.user_name == user_name;
|
||||
let same_email = p.user_email == user_email;
|
||||
let reason = match (same_name, same_email) {
|
||||
(true, true) => "same name & email",
|
||||
(true, false) => "same name",
|
||||
(false, true) => "same email",
|
||||
(false, false) => "partial match",
|
||||
};
|
||||
println!(" - {} ({})", p.name.cyan(), reason.dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
if merged_config.is_complete() {
|
||||
println!("\n {} Would you like to save this identity as a new profile?", "💡".yellow());
|
||||
let save = Confirm::new()
|
||||
.with_prompt("Save current git identity as new profile?")
|
||||
.default(true)
|
||||
.interact()?;
|
||||
|
||||
if save {
|
||||
self.save_current_identity_as_profile(&merged_config, manager).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(profile_name) = name {
|
||||
if let Some(profile) = manager.get_profile(profile_name) {
|
||||
println!("\n{}", format!("Requested Profile: {}", profile_name).bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
self.print_profile_details(profile);
|
||||
} else {
|
||||
println!("\n{} Profile '{}' not found", "✗".red(), profile_name);
|
||||
}
|
||||
} else if let Some(profile) = manager.default_profile() {
|
||||
println!("\n{}", format!("Default Profile: {}", profile.name).bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
self.print_profile_details(profile);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_global_status(&self, manager: &ConfigManager, name: Option<&str>) -> Result<()> {
|
||||
println!("{}", "\n⚠ Not in a Git Repository".bold().yellow());
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("Run this command inside a git repository to see local config status.");
|
||||
println!();
|
||||
|
||||
if let Some(profile_name) = name {
|
||||
if let Some(profile) = manager.get_profile(profile_name) {
|
||||
println!("{}", format!("Profile: {}", profile_name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
self.print_profile_details(profile);
|
||||
} else {
|
||||
bail!("Profile '{}' not found", profile_name);
|
||||
}
|
||||
} else if let Some(profile) = manager.default_profile() {
|
||||
println!("{}", format!("Default Profile: {}", profile.name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
self.print_profile_details(profile);
|
||||
} else {
|
||||
manager.default_profile()
|
||||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||||
println!("{}", "No default profile set.".yellow());
|
||||
println!("Run {} to create one.", "quicommit profile add".cyan());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_config_entry(&self, label: &str, entry: &crate::git::ConfigEntry) {
|
||||
use crate::git::ConfigSource;
|
||||
|
||||
let source_indicator = match entry.source {
|
||||
ConfigSource::Local => format!("[{}]", "local".green()),
|
||||
ConfigSource::Global => format!("[{}]", "global".blue()),
|
||||
ConfigSource::NotSet => format!("[{}]", "not set".dimmed()),
|
||||
};
|
||||
|
||||
println!("{}", format!("\nProfile: {}", profile.name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
match &entry.value {
|
||||
Some(value) => {
|
||||
println!("{} {}: {}", source_indicator, label, value);
|
||||
if entry.local_value.is_some() && entry.global_value.is_some() {
|
||||
println!(" {} local: {}", "├".dimmed(), entry.local_value.as_ref().unwrap());
|
||||
println!(" {} global: {}", "└".dimmed(), entry.global_value.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("{} {}: {}", source_indicator, label, "<not set>".dimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_profile_details(&self, profile: &GitProfile) {
|
||||
println!("User name: {}", profile.user_name);
|
||||
println!("User email: {}", profile.user_email);
|
||||
|
||||
@@ -384,6 +541,112 @@ impl ProfileCommand {
|
||||
println!(" Last used: {}", last_used);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_current_identity_as_profile(&self, merged_config: &crate::git::MergedUserConfig, manager: &ConfigManager) -> Result<()> {
|
||||
let config_path = manager.path().to_path_buf();
|
||||
let mut manager = ConfigManager::with_path(&config_path)?;
|
||||
|
||||
let user_name = merged_config.name.value.clone().unwrap_or_default();
|
||||
let user_email = merged_config.email.value.clone().unwrap_or_default();
|
||||
|
||||
println!("\n{}", "Save New Profile".bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
|
||||
let default_name = user_name.to_lowercase().replace(' ', "-");
|
||||
let profile_name: String = Input::new()
|
||||
.with_prompt("Profile name")
|
||||
.default(default_name)
|
||||
.validate_with(|input: &String| {
|
||||
validate_profile_name(input).map_err(|e| e.to_string())
|
||||
})
|
||||
.interact_text()?;
|
||||
|
||||
if manager.has_profile(&profile_name) {
|
||||
let overwrite = Confirm::new()
|
||||
.with_prompt(&format!("Profile '{}' already exists. Overwrite?", profile_name))
|
||||
.default(false)
|
||||
.interact()?;
|
||||
if !overwrite {
|
||||
println!("{}", "Cancelled.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let description: String = Input::new()
|
||||
.with_prompt("Description (optional)")
|
||||
.default(format!("Imported from existing git config"))
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
let is_work = Confirm::new()
|
||||
.with_prompt("Is this a work profile?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
let organization = if is_work {
|
||||
Some(Input::new()
|
||||
.with_prompt("Organization")
|
||||
.interact_text()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut profile = GitProfile::new(profile_name.clone(), user_name, user_email);
|
||||
if !description.is_empty() {
|
||||
profile.description = Some(description);
|
||||
}
|
||||
profile.is_work = is_work;
|
||||
profile.organization = organization;
|
||||
|
||||
if let Some(ref key) = merged_config.signing_key.value {
|
||||
profile.signing_key = Some(key.clone());
|
||||
|
||||
let setup_gpg = Confirm::new()
|
||||
.with_prompt("Configure GPG signing details?")
|
||||
.default(true)
|
||||
.interact()?;
|
||||
|
||||
if setup_gpg {
|
||||
profile.gpg = Some(self.setup_gpg_interactive().await?);
|
||||
}
|
||||
}
|
||||
|
||||
if merged_config.ssh_command.is_set() {
|
||||
let setup_ssh = Confirm::new()
|
||||
.with_prompt("Configure SSH key details?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if setup_ssh {
|
||||
profile.ssh = Some(self.setup_ssh_interactive().await?);
|
||||
}
|
||||
}
|
||||
|
||||
let setup_token = Confirm::new()
|
||||
.with_prompt("Add a Personal Access Token?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if setup_token {
|
||||
self.setup_token_interactive(&mut profile, &manager).await?;
|
||||
}
|
||||
|
||||
manager.add_profile(profile_name.clone(), profile)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Profile '{}' saved successfully", "✓".green(), profile_name.cyan());
|
||||
|
||||
let set_default = Confirm::new()
|
||||
.with_prompt("Set as default profile?")
|
||||
.default(true)
|
||||
.interact()?;
|
||||
|
||||
if set_default {
|
||||
manager.set_default_profile(Some(profile_name.clone()))?;
|
||||
manager.save()?;
|
||||
println!("{} Set '{}' as default profile", "✓".green(), profile_name.cyan());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -578,6 +841,10 @@ impl ProfileCommand {
|
||||
bail!("Profile '{}' not found", profile_name);
|
||||
}
|
||||
|
||||
if !manager.keyring().is_available() {
|
||||
bail!("Keyring is not available. Cannot store PAT securely. Please ensure your system keyring is accessible.");
|
||||
}
|
||||
|
||||
println!("{}", format!("\nAdd token to profile '{}'", profile_name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
|
||||
@@ -605,15 +872,17 @@ impl ProfileCommand {
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
let mut token = TokenConfig::new(token_value, token_type);
|
||||
let mut token = TokenConfig::new(token_type);
|
||||
if !description.is_empty() {
|
||||
token.description = Some(description);
|
||||
}
|
||||
|
||||
manager.store_pat_for_profile(profile_name, service, &token_value)?;
|
||||
|
||||
manager.add_token_to_profile(profile_name, service.to_string(), token)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Token for '{}' added to profile '{}'", "✓".green(), service.cyan(), profile_name);
|
||||
println!("{} Token for '{}' added to profile '{}' (stored securely in keyring)", "✓".green(), service.cyan(), profile_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -638,7 +907,7 @@ impl ProfileCommand {
|
||||
manager.remove_token_from_profile(profile_name, service)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Token '{}' removed from profile '{}'", "✓".green(), service, profile_name);
|
||||
println!("{} Token '{}' removed from profile '{}' (deleted from keyring)", "✓".green(), service, profile_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -658,7 +927,14 @@ impl ProfileCommand {
|
||||
println!("{}", "─".repeat(40));
|
||||
|
||||
for (service, token) in &profile.tokens {
|
||||
println!("{} ({})", service.cyan().bold(), token.token_type);
|
||||
let has_token = manager.has_pat_for_profile(profile_name, service);
|
||||
let status = if has_token {
|
||||
format!("[{}]", "stored".green())
|
||||
} else {
|
||||
format!("[{}]", "not stored".yellow())
|
||||
};
|
||||
|
||||
println!("{} {} ({})", service.cyan().bold(), status, token.token_type);
|
||||
if let Some(ref desc) = token.description {
|
||||
println!(" {}", desc);
|
||||
}
|
||||
@@ -785,7 +1061,18 @@ impl ProfileCommand {
|
||||
})
|
||||
}
|
||||
|
||||
async fn setup_token_interactive(&self, profile: &mut GitProfile) -> Result<()> {
|
||||
async fn setup_token_interactive(&self, profile: &mut GitProfile, manager: &ConfigManager) -> Result<()> {
|
||||
if !manager.keyring().is_available() {
|
||||
println!("{} Keyring is not available. Cannot store PAT securely.", "⚠".yellow());
|
||||
let continue_anyway = Confirm::new()
|
||||
.with_prompt("Continue without secure token storage?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
if !continue_anyway {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let service: String = Input::new()
|
||||
.with_prompt("Service name (e.g., github, gitlab)")
|
||||
.interact_text()?;
|
||||
@@ -794,7 +1081,13 @@ impl ProfileCommand {
|
||||
.with_prompt("Token value")
|
||||
.interact_text()?;
|
||||
|
||||
let token = TokenConfig::new(token_value, TokenType::Personal);
|
||||
let token = TokenConfig::new(TokenType::Personal);
|
||||
|
||||
if manager.keyring().is_available() {
|
||||
manager.store_pat_for_profile(&profile.name, &service, &token_value)?;
|
||||
println!("{} Token stored securely in keyring", "✓".green());
|
||||
}
|
||||
|
||||
profile.add_token(service, token);
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user