Arrea.Command (Arrea v1.0.0)

Copy Markdown View Source

Ejecución síncrona de comandos con soporte para gestión de versiones.

Proporciona una interfaz limpia para ejecutar comandos shell con validación, selección de shell, detección automática de archivos de configuración, integración con asdf/mise y parseo estructurado de resultados.

Para ejecución en paralelo, usar Arrea.Parallel o Arrea.Leader.

Resolución de shell

El shell se resuelve con la siguiente prioridad (de menor a mayor):

  1. Arrea.Config.get(:shell) — config del proyecto consumidor o runtime
  2. Variable de entorno $SHELL
  3. Login shell del usuario actual (/etc/passwd)
  4. Opción :shell pasada a execute/2máxima prioridad
  5. "sh" como último recurso si ninguno de los anteriores es válido

Se aceptan tanto nombres ("zsh") como rutas ("/bin/zsh"). Los nombres se resuelven a rutas mediante System.find_executable/1. Si el shell configurado no existe, cae al default del sistema ($SHELL o "sh").

Según el shell detectado, se fuerza la carga de su archivo de configuración (ej: ~/.bashrc para bash, ~/.zshrc para zsh, ~/.config/fish/config.fish para fish).

Timeout real

El timeout se aplica durante la ejecución: si el comando no termina en timeout ms, el proceso de ejecución es cancelado. El proceso OS subyacente recibe SIGKILL cuando el puerto de Erlang es cerrado al morir el proceso propietario.

Gestión de versiones asdf/mise

Se pueden forzar versiones de runtimes mediante dos mecanismos:

  • asdf — opción asdf_<lenguaje>: genera export ASDF_<LANG>_VERSION=<version> antes del comando. Funciona tanto con asdf como con mise.

  • mise — opción mise_<lenguaje>: envuelve el comando con mise exec <lenguaje>@<version> -- <comando>.

Ejemplos

iex> Arrea.Command.execute("echo hello")
{:ok, %{stdout: "hello\n", exit_code: 0, duration_ms: 3}}

iex> Arrea.Command.execute("mix test", asdf_elixir: "1.18.0")
{:ok, %{stdout: "...", exit_code: 0, duration_ms: 1200}}

iex> Arrea.Command.execute("node -v", mise_node: "20.0.0")
{:ok, %{stdout: "v20.0.0\n", exit_code: 0, duration_ms: 80}}

iex> Arrea.Command.execute("sleep 60", timeout: 500)
{:error, :timeout}

Summary

Functions

Extrae los argumentos mise_<lang> de las opciones y los formatea como "lang@version" para el comando mise exec.

Ejecuta una cadena de comando de forma síncrona con configuración opcional.

Ejecuta un comando con una versión de lenguaje gestionada por ASDF.

Parsea un mapa de resultado crudo a una forma estructurada.

Resuelve el shell a usar según la prioridad (de menor a mayor)

Resuelve la ruta al archivo de configuración del shell.

Resuelve el nombre de un shell a su ruta absoluta.

Types

result()

@type result() :: %{
  stdout: String.t(),
  exit_code: non_neg_integer(),
  duration_ms: non_neg_integer()
}

Functions

build_mise_args(opts)

@spec build_mise_args(keyword()) :: [String.t()]

Extrae los argumentos mise_<lang> de las opciones y los formatea como "lang@version" para el comando mise exec.

execute(cmd, opts \\ [])

@spec execute(
  String.t(),
  keyword()
) :: {:ok, result()} | {:error, term()}

Ejecuta una cadena de comando de forma síncrona con configuración opcional.

El comando se valida antes de la ejecución. Comandos inválidos o peligrosos retornan {:error, razon} sin ejecutar nada.

El timeout es real: si el comando no termina dentro del límite, el proceso de ejecución es cancelado activamente (no post-hoc).

Opciones

  • :timeout — Tiempo máximo de ejecución en ms (default: 30_000)
  • :cd — Directorio de trabajo (default: directorio actual)
  • :shell — Shell a usar — tiene prioridad máxima sobre config y env
  • :shell_config — Ruta al archivo de configuración del shell a cargar (opcional)
  • :env — Variables de entorno adicionales como mapa (opcional)
  • :quiet — Si es true, suprime la captura de stderr (default: false)
  • :asdf_elixir — Forzar versión de Elixir via asdf/mise
  • asdf_<lang> — Forzar versión de cualquier lenguaje via asdf/mise
  • mise_<lang> — Forzar versión via mise exec

Retorna

  • {:ok, result} — Mapa con :stdout, :exit_code, :duration_ms
  • {:error, :timeout} — El comando fue cancelado por exceder el timeout
  • {:error, reason} — Error de validación o ejecución

execute_with_asdf(cmd, language, version, opts \\ [])

@spec execute_with_asdf(String.t(), atom(), String.t(), keyword()) ::
  {:ok, result()} | {:error, term()}

Ejecuta un comando con una versión de lenguaje gestionada por ASDF.

Envoltorio conveniente para execute/2 que antepone la activación del shim de asdf.

Ejemplos

iex> Command.execute_with_asdf("mix test", :elixir, "1.18.0")
{:ok, %{stdout: "...", exit_code: 0, duration_ms: 1200}}

parse_result(result)

@spec parse_result(result()) ::
  {:ok, result()} | {:error, {:exit_code, non_neg_integer()}}

Parsea un mapa de resultado crudo a una forma estructurada.

Detecta patrones comunes de error y retorna resultados etiquetados.

resolve_shell(opts \\ [])

@spec resolve_shell(keyword()) :: String.t()

Resuelve el shell a usar según la prioridad (de menor a mayor):

  1. Arrea.Config.get(:shell) (config del proyecto o Config.set/2)
  2. Variable de entorno $SHELL
  3. Login shell del usuario en /etc/passwd
  4. Opción :shell pasada en opts — máxima prioridad
  5. "sh" como fallback si ninguno es válido

Si el shell resuelto es un nombre (ej: "zsh"), lo busca en PATH. Si no se encuentra, cae al default del sistema.

resolve_shell_config(shell)

@spec resolve_shell_config(String.t()) :: String.t() | nil

Resuelve la ruta al archivo de configuración del shell.

Retorna la ruta expandida al archivo de config (ej: ~/.zshrc para zsh) o nil si el shell no tiene un archivo de config conocido.

resolve_shell_path(shell)

@spec resolve_shell_path(String.t()) :: String.t() | nil

Resuelve el nombre de un shell a su ruta absoluta.

Si ya es una ruta (contiene /), se devuelve tal cual si existe. Si es solo un nombre, se busca en PATH via System.find_executable/1. Retorna nil si no se encuentra el ejecutable.