diff --git a/helix-cli/Cargo.toml b/helix-cli/Cargo.toml index 436ef48b..bca32a48 100644 --- a/helix-cli/Cargo.toml +++ b/helix-cli/Cargo.toml @@ -32,4 +32,4 @@ path = "src/main.rs" [features] normal = ["helix-db/server"] ingestion = ["helix-db/full"] -default = ["normal"] +default = ["normal"] \ No newline at end of file diff --git a/helix-cli/src/commands/add.rs b/helix-cli/src/commands/add.rs index a85089e8..15ad7af0 100644 --- a/helix-cli/src/commands/add.rs +++ b/helix-cli/src/commands/add.rs @@ -122,6 +122,7 @@ pub async fn run(deployment_type: CloudDeploymentTypeCommand) -> Result<()> { let local_config = LocalInstanceConfig { port: None, // Let the system assign a port build_mode: BuildMode::Debug, + env_file: None, db_config: DbConfig::default(), }; diff --git a/helix-cli/src/commands/integrations/fly.rs b/helix-cli/src/commands/integrations/fly.rs index 8937d82f..6021badc 100644 --- a/helix-cli/src/commands/integrations/fly.rs +++ b/helix-cli/src/commands/integrations/fly.rs @@ -422,7 +422,7 @@ impl<'a> FlyManager<'a> { docker.push(image_name, FLY_REGISTRY_URL)?; // Get environment variables first to ensure they live long enough - let env_vars = docker.environment_variables(instance_name); + let env_vars = docker.environment_variables(instance_name)?; let mut deploy_args = vec![ "deploy", diff --git a/helix-cli/src/commands/migrate.rs b/helix-cli/src/commands/migrate.rs index 6b770760..5f52b5b0 100644 --- a/helix-cli/src/commands/migrate.rs +++ b/helix-cli/src/commands/migrate.rs @@ -414,6 +414,7 @@ fn create_v2_config(ctx: &MigrationContext) -> Result<()> { let local_config = LocalInstanceConfig { port: Some(ctx.port), build_mode: BuildMode::Debug, + env_file: None, db_config, }; diff --git a/helix-cli/src/config.rs b/helix-cli/src/config.rs index 5d282a4e..11ac16ff 100644 --- a/helix-cli/src/config.rs +++ b/helix-cli/src/config.rs @@ -83,6 +83,8 @@ pub struct LocalInstanceConfig { pub port: Option, #[serde(default = "default_dev_build_mode")] pub build_mode: BuildMode, + #[serde(default)] + pub env_file: Option, #[serde(flatten)] pub db_config: DbConfig, } @@ -378,6 +380,7 @@ impl HelixConfig { LocalInstanceConfig { port: Some(6969), build_mode: BuildMode::Debug, + env_file: None, db_config: DbConfig::default(), }, ); diff --git a/helix-cli/src/docker.rs b/helix-cli/src/docker.rs index 1ccf48c5..d121015c 100644 --- a/helix-cli/src/docker.rs +++ b/helix-cli/src/docker.rs @@ -1,4 +1,5 @@ use crate::config::{BuildMode, InstanceInfo}; +use crate::env_utils::load_env_variables; use crate::project::ProjectContext; use crate::utils::print_status; use eyre::{Result, eyre}; @@ -44,25 +45,32 @@ impl<'a> DockerManager<'a> { } /// Get environment variables for an instance - pub(crate) fn environment_variables(&self, instance_name: &str) -> Vec { - vec![ - { - let port = self - .project - .config - .get_instance(instance_name) - .unwrap() - .port() - .unwrap_or(6969); - format!("HELIX_PORT={port}") - }, + pub(crate) fn environment_variables(&self, instance_name: &str) -> Result> { + let instance = self.project.config.get_instance(instance_name)?; + + // Start with Helix built-in variables (highest priority) + let mut env_vars = vec![ + format!("HELIX_PORT={}", instance.port().unwrap_or(6969)), format!("HELIX_DATA_DIR={HELIX_DATA_DIR}"), format!("HELIX_INSTANCE={instance_name}"), - { - let project_name = &self.project.config.project.name; - format!("HELIX_PROJECT={project_name}") - }, - ] + format!("HELIX_PROJECT={}", self.project.config.project.name), + ]; + + // Load user-defined environment variables if this is a local instance + if let InstanceInfo::Local(local_config) = instance + && let Some(env_file) = &local_config.env_file + { + let user_vars = load_env_variables(Some(env_file.as_path()), &self.project.root)?; + + // Add user variables that don't conflict with Helix built-ins + for (key, value) in user_vars { + if !key.starts_with("HELIX_") { + env_vars.push(format!("{key}={value}")); + } + } + } + + Ok(env_vars) } /// Get the container name for an instance @@ -220,6 +228,14 @@ CMD ["helix-container"] let container_name = self.container_name(instance_name); let network_name = self.network_name(instance_name); + // Get environment variables including user-defined ones + let env_vars = self.environment_variables(instance_name)?; + let env_section = env_vars + .iter() + .map(|var| format!(" - {var}")) + .collect::>() + .join("\n"); + let compose = format!( r#"# Generated docker-compose.yml for Helix instance: {instance_name} services: @@ -235,10 +251,7 @@ services: volumes: - ../.volumes/{instance_name}:/data environment: - - HELIX_PORT={port} - - HELIX_DATA_DIR={data_dir} - - HELIX_INSTANCE={instance_name} - - HELIX_PROJECT={project_name} +{env_section} restart: unless-stopped networks: - {network_name} @@ -250,8 +263,6 @@ networks: platform = instance_config .docker_build_target() .map_or("".to_string(), |p| format!("platforms:\n - {p}")), - project_name = self.project.config.project.name, - data_dir = HELIX_DATA_DIR, ); Ok(compose) diff --git a/helix-cli/src/env_utils.rs b/helix-cli/src/env_utils.rs new file mode 100644 index 00000000..ed12525a --- /dev/null +++ b/helix-cli/src/env_utils.rs @@ -0,0 +1,70 @@ +use eyre::Result; +use std::collections::HashMap; +use std::path::Path; + +/// Load environment variables from .env file and shell environment +/// +/// Precedence order (highest to lowest): +/// 1. Shell environment variables +/// 2. .env file variables +pub fn load_env_variables( + env_file_path: Option<&Path>, + project_root: &Path, +) -> Result> { + let mut vars = HashMap::new(); + + // Load from .env file if specified + if let Some(env_path) = env_file_path { + let full_path = project_root.join(env_path); + if full_path.exists() { + // Parse the .env file with improved handling + let env_content = std::fs::read_to_string(&full_path)?; + for (line_num, line) in env_content.lines().enumerate() { + let trimmed = line.trim(); + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + + // Parse KEY=VALUE pairs (find first = to handle values with = in them) + if let Some(eq_pos) = trimmed.find('=') { + let key = trimmed[..eq_pos].trim(); + let value = trimmed[eq_pos + 1..].trim(); + + // Validate key + if key.is_empty() { + eprintln!("Warning: Skipping empty key at line {} in {}", + line_num + 1, full_path.display()); + continue; + } + + // Remove surrounding quotes if present (both single and double) + let value = if (value.starts_with('"') && value.ends_with('"')) + || (value.starts_with('\'') && value.ends_with('\'')) { + &value[1..value.len() - 1] + } else { + value + }; + + vars.insert(key.to_string(), value.to_string()); + } else if !trimmed.starts_with("export ") { + // Skip lines that start with "export " (shell syntax) + eprintln!("Warning: Skipping malformed line {} in {}: {}", + line_num + 1, full_path.display(), trimmed); + } + } + } else { + eprintln!("Warning: env_file '{}' not found, skipping", full_path.display()); + } + } + + // Add shell environment variables (higher priority - overwrites .env) + for (key, value) in std::env::vars() { + // Skip HELIX_ prefixed variables as they're managed internally + if !key.starts_with("HELIX_") { + vars.insert(key, value); + } + } + + Ok(vars) +} \ No newline at end of file diff --git a/helix-cli/src/main.rs b/helix-cli/src/main.rs index 18e279fd..87e0bd0e 100644 --- a/helix-cli/src/main.rs +++ b/helix-cli/src/main.rs @@ -4,6 +4,7 @@ use eyre::Result; mod commands; mod config; mod docker; +mod env_utils; mod errors; mod metrics_sender; mod project;