defmodule Nex.Env do @moduledoc """ Environment variable management. Loads from .env files and system environment. """ require Logger @doc """ Initialize environment from .env files. Options: - :env - Override the environment name (default: auto-detected) - :project_root - Override the project root path (default: auto-detected) """ def init(opts \\ []) do env = opts[:env] || current_env() project_root = opts[:project_root] || detect_project_root() files = [".env", ".env.#{env}"] |> Enum.map(&Path.join(project_root, &1)) |> Enum.filter(&File.exists?/1) if files != [] do Logger.info("[Nex.Env] Loading environment from: #{Enum.join(files, ", ")}") case Dotenvy.source(files) do {:ok, vars} -> Enum.each(vars, fn {key, value} -> System.put_env(key, value) end) Logger.info("[Nex.Env] Environment loaded (#{map_size(vars)} variables)") {:error, reason} -> Logger.warning("[Nex.Env] Failed to load environment: #{inspect(reason)}") end else Logger.debug("[Nex.Env] No .env files found in: #{project_root}") end :ok end @doc """ Returns the current environment name as a string. Uses Mix.env() when available (dev/test), falls back to MIX_ENV env var, then defaults to "prod" - safe for compiled releases. Can be overridden for testing with :mix_env and :system_env options. """ def current_env(opts \\ []) do mix_env = if Keyword.has_key?(opts, :mix_env), do: opts[:mix_env], else: detect_mix_env() system_env = opts[:system_env] || (&System.get_env/1) if mix_env do mix_env |> to_string() else system_env.("MIX_ENV") || "prod" end end @doc """ Detects project root directory. In dev/test (Mix available): walks up from _build app path. In prod release (no Mix): uses the directory of the running executable or cwd. Can be overridden for testing with :mix_project_path and :progname options. """ def detect_project_root(opts \\ []) do mix_project_path = if Keyword.has_key?(opts, :mix_project_path), do: opts[:mix_project_path], else: detect_mix_project_path() progname = if Keyword.has_key?(opts, :progname), do: opts[:progname], else: detect_progname() cond do mix_project_path -> mix_project_path |> Path.dirname() |> Path.dirname() |> Path.dirname() |> Path.dirname() progname -> progname |> to_string() |> Path.expand() |> Path.dirname() true -> File.cwd!() end end @doc "Get environment variable with optional default" def get(key, default \\ nil) when is_atom(key) do key_str = key |> to_string() |> String.upcase() System.get_env(key_str) || default end @doc "Get required environment variable (raises if missing)" def get!(key) when is_atom(key) do case get(key) do nil -> raise "Missing required environment variable: #{key}" value -> value end end @doc "Get environment variable as integer" def get_integer(key, default) when is_atom(key) do case get(key) do nil -> default value -> case Integer.parse(value) do {n, ""} -> n _ -> default end end end @doc "Get environment variable as boolean" def get_boolean(key, default) when is_atom(key) do case get(key) do nil -> default "true" -> true "1" -> true _ -> false end end defp detect_mix_env do if function_exported?(Mix, :env, 0) do Mix.env() else nil end end defp detect_mix_project_path do if function_exported?(Mix.Project, :app_path, 0) do case Mix.Project.app_path() do nil -> nil app_path -> app_path end else nil end end defp detect_progname do case :init.get_argument(:progname) do {:ok, [[progname]]} -> progname _ -> nil end end end