use crate::commands::Cli;
use crate::config::Config;
use crate::error::{FnoxError, Result};
use crate::providers::{OptionStringOrSecretRef, StringOrSecretRef};
use clap::Args;
use super::ProviderType;
#[derive(Debug, Args)]
#[command(visible_aliases = ["a", "set"])]
pub struct AddCommand {
pub provider: String,
#[arg(value_enum)]
pub provider_type: ProviderType,
#[arg(short = 'g', long)]
pub global: bool,
#[arg(long)]
pub vault: Option<String>,
}
impl AddCommand {
pub async fn run(&self, cli: &Cli) -> Result<()> {
tracing::debug!(
"Adding provider '{}' of type '{}'",
self.provider,
self.provider_type
);
if self.vault.is_some() && self.provider_type != ProviderType::ProtonPass {
return Err(FnoxError::Config(
"--vault is only supported for provider type 'proton-pass'".to_string(),
));
}
let target_path = if self.global {
let global_path = Config::global_config_path();
if let Some(parent) = global_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
FnoxError::Config(format!(
"Failed to create config directory '{}': {}",
parent.display(),
e
))
})?;
}
global_path
} else {
let current_dir = std::env::current_dir().map_err(|e| {
FnoxError::Config(format!("Failed to get current directory: {}", e))
})?;
current_dir.join(&cli.config)
};
let mut config = if target_path.exists() {
Config::load(&target_path)?
} else {
Config::new()
};
if config.providers.contains_key(&self.provider) {
return Err(FnoxError::Config(format!(
"Provider '{}' already exists",
self.provider
)));
}
let provider_config = match self.provider_type {
ProviderType::OnePassword => crate::config::ProviderConfig::OnePassword {
vault: OptionStringOrSecretRef::literal("default"),
account: OptionStringOrSecretRef::none(),
token: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Aws => crate::config::ProviderConfig::AwsSecretsManager {
region: StringOrSecretRef::from("us-east-1"),
profile: OptionStringOrSecretRef::none(),
prefix: OptionStringOrSecretRef::none(),
endpoint: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Vault => crate::config::ProviderConfig::HashiCorpVault {
address: OptionStringOrSecretRef::literal("http://localhost:8200"),
path: OptionStringOrSecretRef::literal("secret"),
token: OptionStringOrSecretRef::none(),
namespace: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Gcp => crate::config::ProviderConfig::GoogleSecretManager {
project: StringOrSecretRef::from("my-project"),
prefix: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::AwsKms => crate::config::ProviderConfig::AwsKms {
region: StringOrSecretRef::from("us-east-1"),
key_id: StringOrSecretRef::from("alias/my-key"),
endpoint: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::AwsParameterStore => crate::config::ProviderConfig::AwsParameterStore {
region: StringOrSecretRef::from("us-east-1"),
profile: OptionStringOrSecretRef::none(),
prefix: OptionStringOrSecretRef::literal("/myapp/prod/"),
endpoint: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::AzureKms => crate::config::ProviderConfig::AzureKms {
vault_url: StringOrSecretRef::from("https://my-vault.vault.azure.net/"),
key_name: StringOrSecretRef::from("my-key"),
auth_command: None,
},
ProviderType::AzureSecretsManager => {
crate::config::ProviderConfig::AzureSecretsManager {
vault_url: StringOrSecretRef::from("https://my-vault.vault.azure.net/"),
prefix: OptionStringOrSecretRef::none(),
auth_command: None,
}
}
ProviderType::GcpKms => crate::config::ProviderConfig::GcpKms {
project: StringOrSecretRef::from("my-project"),
location: StringOrSecretRef::from("global"),
keyring: StringOrSecretRef::from("my-keyring"),
key: StringOrSecretRef::from("my-key"),
auth_command: None,
},
ProviderType::Bitwarden => crate::config::ProviderConfig::Bitwarden {
collection: OptionStringOrSecretRef::none(),
organization_id: OptionStringOrSecretRef::none(),
profile: OptionStringOrSecretRef::none(),
backend: None,
auth_command: None,
},
ProviderType::BitwardenSecretsManager => {
crate::config::ProviderConfig::BitwardenSecretsManager {
project_id: OptionStringOrSecretRef::none(),
profile: OptionStringOrSecretRef::none(),
auth_command: None,
}
}
ProviderType::Age => crate::config::ProviderConfig::AgeEncryption {
recipients: vec!["age1...".to_string()],
key_file: OptionStringOrSecretRef::none(),
auth_command: None,
},
#[cfg(not(target_env = "musl"))]
ProviderType::Fido2 => {
let provider_name = self.provider.clone();
let (credential_id_hex, salt_hex, rp_id, _pin) =
tokio::task::spawn_blocking(move || {
crate::providers::fido2::setup::setup_fido2(&provider_name)
})
.await
.map_err(|e| FnoxError::Provider(format!("FIDO2 setup task failed: {e}")))??;
crate::config::ProviderConfig::Fido2 {
credential_id: StringOrSecretRef::from(credential_id_hex.as_str()),
salt: StringOrSecretRef::from(salt_hex.as_str()),
rp_id: StringOrSecretRef::from(rp_id.as_str()),
pin: OptionStringOrSecretRef::none(),
auth_command: None,
}
}
ProviderType::Yubikey => {
let provider_name = self.provider.clone();
let (challenge_hex, slot_str) = tokio::task::spawn_blocking(move || {
crate::providers::yubikey::setup::setup_yubikey(&provider_name)
})
.await
.map_err(|e| FnoxError::Provider(format!("YubiKey setup task failed: {e}")))??;
crate::config::ProviderConfig::Yubikey {
challenge: StringOrSecretRef::from(challenge_hex.as_str()),
slot: StringOrSecretRef::from(slot_str.as_str()),
auth_command: None,
}
}
ProviderType::Doppler => crate::config::ProviderConfig::Doppler {
project: OptionStringOrSecretRef::literal("my-project"),
config: OptionStringOrSecretRef::literal("prd"),
token: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Infisical => crate::config::ProviderConfig::Infisical {
project_id: OptionStringOrSecretRef::literal("your-project-id"),
environment: OptionStringOrSecretRef::literal("dev"),
path: OptionStringOrSecretRef::literal("/"),
auth_command: None,
},
ProviderType::KeePass => crate::config::ProviderConfig::KeePass {
database: StringOrSecretRef::from("~/secrets.kdbx"),
keyfile: OptionStringOrSecretRef::none(),
password: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Keychain => crate::config::ProviderConfig::Keychain {
service: StringOrSecretRef::from("fnox"),
prefix: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::PasswordStore => crate::config::ProviderConfig::PasswordStore {
prefix: OptionStringOrSecretRef::literal("fnox/"),
store_dir: OptionStringOrSecretRef::none(),
gpg_opts: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Passwordstate => crate::config::ProviderConfig::Passwordstate {
base_url: StringOrSecretRef::from("https://passwordstate.example.com"),
api_key: OptionStringOrSecretRef::none(),
password_list_id: StringOrSecretRef::from("123"),
verify_ssl: OptionStringOrSecretRef::none(),
auth_command: None,
},
ProviderType::Plain => crate::config::ProviderConfig::Plain { auth_command: None },
ProviderType::ProtonPass => crate::config::ProviderConfig::ProtonPass {
vault: self
.vault
.as_ref()
.map_or_else(OptionStringOrSecretRef::none, |vault| {
OptionStringOrSecretRef::literal(vault.clone())
}),
auth_command: None,
},
};
config
.providers
.insert(self.provider.clone(), provider_config);
config.save(&target_path)?;
let global_suffix = if self.global { " (global)" } else { "" };
println!("✓ Added provider '{}'{}", self.provider, global_suffix);
println!(
"\nNote: Please edit '{}' to configure the provider settings.",
target_path.display()
);
Ok(())
}
}