use std::fs;
use std::io;
use std::path::Path;
use colored::Colorize;
use toml_edit::DocumentMut;
use crate::config::{loader, ConfigError};
use crate::crypto::{encrypt_value, CryptoError};
pub fn set_variable(
project_dir: &Path,
environment: &str,
key: &str,
value: Option<String>,
encrypt: bool,
) -> Result<(), SetCommandError> {
let config_path = project_dir.join(".stand.toml");
let config = loader::load_config_toml(project_dir)?;
if !config.environments.contains_key(environment) {
return Err(SetCommandError::EnvironmentNotFound(
environment.to_string(),
));
}
let plain_value = match value {
Some(v) => v,
None if encrypt => prompt_for_secret(key)?,
None => return Err(SetCommandError::ValueRequired),
};
let final_value = if encrypt {
let public_key = get_public_key(&config_path)?;
let recipient = crate::crypto::keys::parse_public_key(&public_key)?;
encrypt_value(&plain_value, &recipient)?
} else {
plain_value
};
update_toml_variable(&config_path, environment, key, &final_value)?;
if encrypt {
println!(
"{} Set {} in [environments.{}] (encrypted)",
"✓".green(),
key,
environment
);
} else {
println!(
"{} Set {} in [environments.{}]",
"✓".green(),
key,
environment
);
}
Ok(())
}
fn prompt_for_secret(key: &str) -> Result<String, SetCommandError> {
let prompt = format!("Enter value for {}: ", key);
rpassword::prompt_password(prompt).map_err(SetCommandError::Io)
}
fn get_public_key(config_path: &Path) -> Result<String, SetCommandError> {
let content = fs::read_to_string(config_path)?;
let doc: DocumentMut = content
.parse()
.map_err(|e: toml_edit::TomlError| SetCommandError::TomlParse(e.to_string()))?;
doc.get("encryption")
.and_then(|e| e.get("public_key"))
.and_then(|k| k.as_str())
.map(|s| s.to_string())
.ok_or(SetCommandError::EncryptionNotEnabled)
}
fn update_toml_variable(
config_path: &Path,
environment: &str,
key: &str,
value: &str,
) -> Result<(), SetCommandError> {
let content = fs::read_to_string(config_path)?;
let mut doc: DocumentMut = content
.parse()
.map_err(|e: toml_edit::TomlError| SetCommandError::TomlParse(e.to_string()))?;
let env_table = doc
.get_mut("environments")
.and_then(|e| e.get_mut(environment))
.and_then(|e| e.as_table_mut())
.ok_or_else(|| SetCommandError::EnvironmentNotFound(environment.to_string()))?;
env_table.insert(key, toml_edit::value(value));
fs::write(config_path, doc.to_string())?;
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum SetCommandError {
#[error("Environment not found: {0}")]
EnvironmentNotFound(String),
#[error("Value is required when not encrypting")]
ValueRequired,
#[error("Encryption is not enabled. Run 'stand encrypt enable' first.")]
EncryptionNotEnabled,
#[error("Cryptographic error: {0}")]
Crypto(#[from] CryptoError),
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("TOML parsing error: {0}")]
TomlParse(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_set_variable_plain() {
let dir = tempdir().unwrap();
let config_path = dir.path().join(".stand.toml");
fs::write(
&config_path,
r#"version = "1.0"
[environments.dev]
description = "Development"
"#,
)
.unwrap();
let result = set_variable(
dir.path(),
"dev",
"API_URL",
Some("https://api.example.com".to_string()),
false,
);
assert!(result.is_ok());
let updated_content = fs::read_to_string(&config_path).unwrap();
assert!(updated_content.contains("API_URL"));
assert!(updated_content.contains("https://api.example.com"));
}
#[test]
fn test_set_variable_env_not_found() {
let dir = tempdir().unwrap();
let config_path = dir.path().join(".stand.toml");
fs::write(
&config_path,
r#"version = "1.0"
[environments.dev]
description = "Development"
"#,
)
.unwrap();
let result = set_variable(
dir.path(),
"prod",
"API_KEY",
Some("secret".to_string()),
false,
);
assert!(matches!(
result,
Err(SetCommandError::EnvironmentNotFound(_))
));
}
#[test]
fn test_set_variable_encrypt_no_key() {
let dir = tempdir().unwrap();
let config_path = dir.path().join(".stand.toml");
fs::write(
&config_path,
r#"version = "1.0"
[environments.dev]
description = "Development"
"#,
)
.unwrap();
let result = set_variable(
dir.path(),
"dev",
"API_KEY",
Some("secret".to_string()),
true,
);
assert!(matches!(result, Err(SetCommandError::EncryptionNotEnabled)));
}
#[test]
fn test_set_variable_encrypted_success() {
let dir = tempdir().unwrap();
let key_pair = crate::crypto::keys::generate_key_pair();
let config_path = dir.path().join(".stand.toml");
fs::write(
&config_path,
format!(
r#"version = "1.0"
[encryption]
public_key = "{}"
[environments.dev]
description = "Development"
"#,
key_pair.public_key
),
)
.unwrap();
let result = set_variable(
dir.path(),
"dev",
"API_KEY",
Some("secret-value".to_string()),
true,
);
assert!(result.is_ok());
let content = fs::read_to_string(&config_path).unwrap();
assert!(
content.contains("encrypted:"),
"Value should be encrypted in config file"
);
assert!(
!content.contains("secret-value"),
"Plain value should not appear in config file"
);
}
}