feat: Auto-carga de variables de entorno desde archivo .env

- Agregada dependencia dotenvy para parsear archivos .env
- Implementada función read_env_file() que lee y parsea archivos .env
- Soporta comentarios (#), líneas vacías, y valores con/sin comillas
- Auto-detección: si existe .env en WorkingDirectory, se carga automáticamente
- Merge inteligente: .env primero, luego variables manuales (sobrescriben)
- Las apps ahora pueden usar su .env sin tener que copiar 17+ variables manualmente
- Logs claros: informa cuántas variables se cargaron desde .env
- Beneficio: registrar apps es mucho más rápido y menos propenso a errores
This commit is contained in:
2026-01-21 17:04:53 -05:00
parent 6fa7b5c86c
commit 2f867cb7ed
3 changed files with 75 additions and 2 deletions

7
Cargo.lock generated
View File

@@ -281,6 +281,12 @@ dependencies = [
"syn",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "either"
version = "1.15.0"
@@ -1332,6 +1338,7 @@ dependencies = [
"axum",
"chrono",
"dashmap",
"dotenvy",
"futures",
"regex",
"reqwest",

View File

@@ -17,6 +17,7 @@ tokio-stream = "0.1"
regex = "1.10"
thiserror = "1.0"
dashmap = "5.5"
dotenvy = "0.15"
[dev-dependencies]
tempfile = "3.8"

View File

@@ -3,6 +3,7 @@ use crate::models::ServiceConfig;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::collections::HashMap;
use crate::logger::get_logger;
pub struct ServiceGenerator;
@@ -74,8 +75,21 @@ impl ServiceGenerator {
format!("{} {}", executable, config.script_path)
};
// Generar variables de entorno del usuario
let mut env_lines: Vec<String> = config.environment
// 1. Leer variables del archivo .env si existe
let env_file_vars = Self::read_env_file(&config.working_directory);
// 2. Merge: .env primero, luego sobrescribir con variables del config
let mut merged_env = env_file_vars.clone();
for (key, value) in &config.environment {
merged_env.insert(key.clone(), value.clone());
}
if !env_file_vars.is_empty() {
logger.info("ServiceGenerator", &format!("📄 Usando {} variables desde .env", env_file_vars.len()));
}
// Generar variables de entorno (desde .env + config manual)
let mut env_lines: Vec<String> = merged_env
.iter()
.map(|(key, value)| format!("Environment=\"{}={}\"", key, value))
.collect();
@@ -295,4 +309,55 @@ WantedBy=multi-user.target
Err(_) => false,
}
}
/// Lee el archivo .env del directorio de trabajo y retorna las variables
pub fn read_env_file(working_directory: &str) -> HashMap<String, String> {
let logger = get_logger();
let env_path = Path::new(working_directory).join(".env");
let mut env_vars = HashMap::new();
if !env_path.exists() {
logger.info("ServiceGenerator", &format!("No se encontró archivo .env en: {}", env_path.display()));
return env_vars;
}
logger.info("ServiceGenerator", &format!("Leyendo archivo .env desde: {}", env_path.display()));
match fs::read_to_string(&env_path) {
Ok(content) => {
for line in content.lines() {
let line = line.trim();
// Ignorar líneas vacías y comentarios
if line.is_empty() || line.starts_with('#') {
continue;
}
// Parsear línea KEY=VALUE
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let mut value = line[pos + 1..].trim().to_string();
// Remover comillas simples o dobles
if (value.starts_with('\'') && value.ends_with('\'')) ||
(value.starts_with('"') && value.ends_with('"')) {
value = value[1..value.len()-1].to_string();
}
if !key.is_empty() {
env_vars.insert(key, value);
}
}
}
logger.info("ServiceGenerator", &format!("✅ Cargadas {} variables desde .env", env_vars.len()));
}
Err(e) => {
logger.warning("ServiceGenerator", &format!("Error leyendo .env: {}", e), None);
}
}
env_vars
}
}