use crate::config::loader;
use crate::crypto::decrypt_variables;
use crate::shell::{get_active_environment, is_stand_shell_active};
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::env;
use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct EnvOptions {
pub json: bool,
pub stand_only: bool,
pub user_only: bool,
}
const STAND_MARKER_VARS: &[&str] = &[
"STAND_ACTIVE",
"STAND_ENVIRONMENT",
"STAND_PROJECT_ROOT",
"STAND_ENV_COLOR",
"STAND_PROMPT",
];
fn get_stand_markers() -> HashMap<String, String> {
let mut markers = HashMap::new();
for var_name in STAND_MARKER_VARS {
if let Ok(value) = env::var(var_name) {
markers.insert(var_name.to_string(), value);
}
}
markers
}
fn get_user_variables(project_path: &Path, env_name: &str) -> Result<HashMap<String, String>> {
let config = loader::load_config_toml_with_inheritance(project_path)?;
let env = config
.environments
.get(env_name)
.ok_or_else(|| anyhow!("Environment '{}' not found in configuration", env_name))?;
let decrypted = decrypt_variables(env.variables.clone(), project_path)
.map_err(|e| anyhow!("Failed to decrypt variables: {}", e))?;
Ok(decrypted)
}
fn format_plain(
stand_markers: &HashMap<String, String>,
user_vars: &HashMap<String, String>,
options: &EnvOptions,
) -> String {
let mut output = String::new();
if !options.user_only && !stand_markers.is_empty() {
output.push_str("# Stand Environment\n");
let mut sorted_markers: Vec<_> = stand_markers.iter().collect();
sorted_markers.sort_by_key(|(k, _)| *k);
for (key, value) in sorted_markers {
output.push_str(&format!("{}={}\n", key, value));
}
}
if !options.stand_only && !user_vars.is_empty() {
if !output.is_empty() {
output.push('\n');
}
output.push_str("# User Variables\n");
let mut sorted_vars: Vec<_> = user_vars.iter().collect();
sorted_vars.sort_by_key(|(k, _)| *k);
for (key, value) in sorted_vars {
output.push_str(&format!("{}={}\n", key, value));
}
}
output
}
fn format_json(
stand_markers: &HashMap<String, String>,
user_vars: &HashMap<String, String>,
options: &EnvOptions,
) -> Result<String> {
use std::collections::BTreeMap;
#[derive(serde::Serialize)]
struct EnvOutput {
#[serde(skip_serializing_if = "Option::is_none")]
stand: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<BTreeMap<String, String>>,
}
let stand = if options.user_only {
None
} else {
Some(
stand_markers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
};
let user = if options.stand_only {
None
} else {
Some(
user_vars
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
};
let output = EnvOutput { stand, user };
Ok(serde_json::to_string_pretty(&output)?)
}
pub fn show_env(project_path: &Path, options: EnvOptions) -> Result<String> {
if !is_stand_shell_active() {
return Err(anyhow!(
"Not inside a Stand subshell.\n\
Use 'stand shell <environment>' to start a subshell first."
));
}
let env_name = get_active_environment().ok_or_else(|| {
anyhow!("STAND_ENVIRONMENT is not set. This should not happen inside a Stand subshell.")
})?;
let stand_markers = get_stand_markers();
let user_vars = if options.stand_only {
HashMap::new()
} else {
get_user_variables(project_path, &env_name)?
};
if options.json {
format_json(&stand_markers, &user_vars, &options)
} else {
Ok(format_plain(&stand_markers, &user_vars, &options))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_get_stand_markers_empty() {
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
let markers = get_stand_markers();
assert!(markers.is_empty() || !markers.contains_key("STAND_ACTIVE"));
}
#[test]
#[serial]
fn test_get_stand_markers_with_vars() {
env::set_var("STAND_ACTIVE", "1");
env::set_var("STAND_ENVIRONMENT", "dev");
let markers = get_stand_markers();
assert_eq!(markers.get("STAND_ACTIVE"), Some(&"1".to_string()));
assert_eq!(markers.get("STAND_ENVIRONMENT"), Some(&"dev".to_string()));
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
}
#[test]
#[serial]
fn test_show_env_not_in_subshell() {
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
let dir = tempdir().unwrap();
let result = show_env(dir.path(), EnvOptions::default());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Not inside a Stand subshell"));
}
#[test]
#[serial]
fn test_show_env_stand_active_but_no_environment() {
env::set_var("STAND_ACTIVE", "1");
env::remove_var("STAND_ENVIRONMENT");
let dir = tempdir().unwrap();
let result = show_env(dir.path(), EnvOptions::default());
env::remove_var("STAND_ACTIVE");
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("STAND_ENVIRONMENT is not set"));
}
#[test]
#[serial]
fn test_show_env_in_subshell() {
env::set_var("STAND_ACTIVE", "1");
env::set_var("STAND_ENVIRONMENT", "dev");
env::set_var("STAND_PROJECT_ROOT", "/test/path");
let dir = tempdir().unwrap();
let config_content = r#"
version = "2.0"
[environments.dev]
description = "Development"
DATABASE_URL = "postgres://localhost/dev"
API_KEY = "dev-key"
"#;
fs::write(dir.path().join(".stand.toml"), config_content).unwrap();
let result = show_env(dir.path(), EnvOptions::default());
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
env::remove_var("STAND_PROJECT_ROOT");
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("STAND_ACTIVE=1"));
assert!(output.contains("STAND_ENVIRONMENT=dev"));
assert!(output.contains("DATABASE_URL=postgres://localhost/dev"));
}
#[test]
#[serial]
fn test_show_env_stand_only() {
env::set_var("STAND_ACTIVE", "1");
env::set_var("STAND_ENVIRONMENT", "dev");
let dir = tempdir().unwrap();
let config_content = r#"
version = "2.0"
[environments.dev]
description = "Development"
DATABASE_URL = "postgres://localhost/dev"
"#;
fs::write(dir.path().join(".stand.toml"), config_content).unwrap();
let options = EnvOptions {
stand_only: true,
..Default::default()
};
let result = show_env(dir.path(), options);
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("STAND_ACTIVE"));
assert!(!output.contains("DATABASE_URL"));
}
#[test]
#[serial]
fn test_show_env_user_only() {
env::set_var("STAND_ACTIVE", "1");
env::set_var("STAND_ENVIRONMENT", "dev");
let dir = tempdir().unwrap();
let config_content = r#"
version = "2.0"
[environments.dev]
description = "Development"
DATABASE_URL = "postgres://localhost/dev"
"#;
fs::write(dir.path().join(".stand.toml"), config_content).unwrap();
let options = EnvOptions {
user_only: true,
..Default::default()
};
let result = show_env(dir.path(), options);
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
assert!(result.is_ok());
let output = result.unwrap();
assert!(!output.contains("STAND_ACTIVE"));
assert!(output.contains("DATABASE_URL"));
}
#[test]
#[serial]
fn test_show_env_json_output() {
env::set_var("STAND_ACTIVE", "1");
env::set_var("STAND_ENVIRONMENT", "dev");
let dir = tempdir().unwrap();
let config_content = r#"
version = "2.0"
[environments.dev]
description = "Development"
API_KEY = "test-key"
"#;
fs::write(dir.path().join(".stand.toml"), config_content).unwrap();
let options = EnvOptions {
json: true,
..Default::default()
};
let result = show_env(dir.path(), options);
env::remove_var("STAND_ACTIVE");
env::remove_var("STAND_ENVIRONMENT");
assert!(result.is_ok());
let output = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed.get("stand").is_some());
assert!(parsed.get("user").is_some());
}
#[test]
fn test_format_plain_output() {
let mut stand_markers = HashMap::new();
stand_markers.insert("STAND_ACTIVE".to_string(), "1".to_string());
stand_markers.insert("STAND_ENVIRONMENT".to_string(), "dev".to_string());
let mut user_vars = HashMap::new();
user_vars.insert("API_KEY".to_string(), "secret".to_string());
let output = format_plain(&stand_markers, &user_vars, &EnvOptions::default());
assert!(output.contains("# Stand Environment"));
assert!(output.contains("STAND_ACTIVE=1"));
assert!(output.contains("# User Variables"));
assert!(output.contains("API_KEY=secret"));
}
}