summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorluisgulo <luisgulo@gmail.com>2025-10-24 18:01:10 +0200
committerluisgulo <luisgulo@gmail.com>2025-10-24 18:01:10 +0200
commit533e79ba959143f0459431a486bfb85c56c72ddc (patch)
tree91974de1bbbdc4c51c76ed591fc5c6e02a3342b6 /core
parent45019c81cfd0fc1d18dce18cdfd5f127c6d61073 (diff)
Releasing code version 1.8.0
Diffstat (limited to 'core')
-rw-r--r--core/inventory/groups.yaml9
-rw-r--r--core/inventory/hosts.yaml17
-rw-r--r--core/inventory/vars/all.yaml20
-rw-r--r--core/lib/translate_msg.sh9
-rw-r--r--core/modules/api.sh88
-rw-r--r--core/modules/api.tr.en8
-rw-r--r--core/modules/api.tr.es8
-rw-r--r--core/modules/archive.sh113
-rw-r--r--core/modules/archive.tr.en14
-rw-r--r--core/modules/archive.tr.es14
-rw-r--r--core/modules/blockinfile.sh79
-rw-r--r--core/modules/blockinfile.tr.en7
-rw-r--r--core/modules/blockinfile.tr.es7
-rw-r--r--core/modules/copy.sh65
-rw-r--r--core/modules/copy.tr.en7
-rw-r--r--core/modules/copy.tr.es7
-rw-r--r--core/modules/cron.sh97
-rw-r--r--core/modules/cron.tr.en12
-rw-r--r--core/modules/cron.tr.es12
-rw-r--r--core/modules/docker.sh81
-rw-r--r--core/modules/docker.tr.en10
-rw-r--r--core/modules/docker.tr.es10
-rw-r--r--core/modules/download.sh64
-rw-r--r--core/modules/download.tr.en7
-rw-r--r--core/modules/download.tr.es7
-rw-r--r--core/modules/echo.sh51
-rw-r--r--core/modules/echo.tr.en2
-rw-r--r--core/modules/echo.tr.es2
-rw-r--r--core/modules/facts.sh121
-rw-r--r--core/modules/facts.tr.en5
-rw-r--r--core/modules/facts.tr.es5
-rw-r--r--core/modules/file.sh78
-rw-r--r--core/modules/file.tr.en8
-rw-r--r--core/modules/file.tr.es8
-rw-r--r--core/modules/file_read.sh56
-rw-r--r--core/modules/file_read.tr.en4
-rw-r--r--core/modules/file_read.tr.es4
-rw-r--r--core/modules/fs.sh91
-rw-r--r--core/modules/fs.tr.en4
-rw-r--r--core/modules/fs.tr.es4
-rw-r--r--core/modules/git.sh88
-rw-r--r--core/modules/git.tr.en7
-rw-r--r--core/modules/git.tr.es7
-rw-r--r--core/modules/groups.sh94
-rw-r--r--core/modules/groups.tr.en15
-rw-r--r--core/modules/groups.tr.es15
-rw-r--r--core/modules/lineinfile.sh91
-rw-r--r--core/modules/lineinfile.tr.en9
-rw-r--r--core/modules/lineinfile.tr.es9
-rw-r--r--core/modules/lookup.sh51
-rw-r--r--core/modules/lookup.tr.en3
-rw-r--r--core/modules/lookup.tr.es3
-rw-r--r--core/modules/loop.sh130
-rw-r--r--core/modules/loop.tr.en7
-rw-r--r--core/modules/loop.tr.es7
-rw-r--r--core/modules/openssl.sh141
-rw-r--r--core/modules/openssl.tr.en20
-rw-r--r--core/modules/openssl.tr.es20
-rw-r--r--core/modules/package.sh132
-rw-r--r--core/modules/package.tr.en10
-rw-r--r--core/modules/package.tr.es10
-rw-r--r--core/modules/ping.sh55
-rw-r--r--core/modules/ping.tr.en5
-rw-r--r--core/modules/ping.tr.es5
-rw-r--r--core/modules/replace.sh67
-rw-r--r--core/modules/replace.tr.en5
-rw-r--r--core/modules/replace.tr.es5
-rw-r--r--core/modules/run.sh90
-rw-r--r--core/modules/run.tr.en4
-rw-r--r--core/modules/run.tr.es4
-rw-r--r--core/modules/service.sh64
-rw-r--r--core/modules/service.tr.en6
-rw-r--r--core/modules/service.tr.es6
-rw-r--r--core/modules/smtp_send.sh94
-rw-r--r--core/modules/smtp_send.tr.en8
-rw-r--r--core/modules/smtp_send.tr.es8
-rw-r--r--core/modules/template.sh131
-rw-r--r--core/modules/template.tr.en5
-rw-r--r--core/modules/template.tr.es5
-rw-r--r--core/modules/user.tr.en15
-rw-r--r--core/modules/user.tr.es15
-rw-r--r--core/modules/users.sh106
-rw-r--r--core/modules/vault-remote.sh81
-rw-r--r--core/modules/vault-remote.tr.en8
-rw-r--r--core/modules/vault-remote.tr.es8
-rw-r--r--core/modules/wait.sh39
-rw-r--r--core/modules/wait.tr.en5
-rw-r--r--core/modules/wait.tr.es5
-rw-r--r--core/templates/.gitignore0
-rwxr-xr-xcore/utils/eg.sh27
-rwxr-xr-xcore/utils/module-docgen.sh69
-rw-r--r--core/utils/module-docgen.tr.en10
-rw-r--r--core/utils/module-docgen.tr.es10
-rwxr-xr-xcore/utils/module-template.sh76
-rw-r--r--core/utils/module-template.tr.en3
-rw-r--r--core/utils/module-template.tr.es3
-rwxr-xr-xcore/utils/shflow-check.sh100
-rw-r--r--core/utils/shflow-check.tr.en12
-rw-r--r--core/utils/shflow-check.tr.es12
-rwxr-xr-xcore/utils/shflow-doc.sh63
-rw-r--r--core/utils/shflow-doc.tr.en9
-rw-r--r--core/utils/shflow-doc.tr.es9
-rwxr-xr-xcore/utils/shflow-ssh-init.sh74
-rw-r--r--core/utils/shflow-ssh-init.tr.en12
-rw-r--r--core/utils/shflow-ssh-init.tr.es12
-rwxr-xr-xcore/utils/shflow-trust.sh90
-rw-r--r--core/utils/shflow-trust.tr.en14
-rw-r--r--core/utils/shflow-trust.tr.es14
-rwxr-xr-xcore/utils/vault-init.sh97
-rw-r--r--core/utils/vault-init.tr.en19
-rw-r--r--core/utils/vault-init.tr.es19
-rwxr-xr-xcore/utils/vault_utils.sh41
-rw-r--r--core/utils/vault_utils.tr.en2
-rw-r--r--core/utils/vault_utils.tr.es2
-rw-r--r--core/vault/.gitignore0
115 files changed, 3732 insertions, 0 deletions
diff --git a/core/inventory/groups.yaml b/core/inventory/groups.yaml
new file mode 100644
index 0000000..52dae9b
--- /dev/null
+++ b/core/inventory/groups.yaml
@@ -0,0 +1,9 @@
+# groups.yaml — Definición de grupos en ShFlow
+# Este archivo permite describir grupos, asignar etiquetas y metadatos.
+# Sintaxis:
+# groups:
+# nombre_grupo:
+# description: <texto descriptivo>
+# tags: [tag1, tag2, ...]
+
+groups: {}
diff --git a/core/inventory/hosts.yaml b/core/inventory/hosts.yaml
new file mode 100644
index 0000000..faafa09
--- /dev/null
+++ b/core/inventory/hosts.yaml
@@ -0,0 +1,17 @@
+# hosts.yaml — Inventario principal de ShFlow
+# Este archivo define los hosts y su pertenencia a grupos.
+# Sintaxis:
+# all:
+# hosts:
+# nombre_host:
+# ansible_host: <IP o FQDN>
+# become: <true|false>
+# <clave>: <valor>
+# children:
+# nombre_grupo:
+# hosts:
+# nombre_host:
+
+all:
+ hosts: {}
+ children: {}
diff --git a/core/inventory/vars/all.yaml b/core/inventory/vars/all.yaml
new file mode 100644
index 0000000..e0c5b8f
--- /dev/null
+++ b/core/inventory/vars/all.yaml
@@ -0,0 +1,20 @@
+# all.yaml — Variables globales para todos los hosts en ShFlow
+# Este archivo define valores comunes que se aplican a todos los hosts del inventario.
+# Sintaxis:
+# clave: valor
+# Las variables aquí definidas pueden ser sobrescritas por vars de grupo o de host.
+
+language: es
+timezone: Europe/Madrid
+ntp_servers:
+ - 0.europe.pool.ntp.org
+ - 1.europe.pool.ntp.org
+default_packages:
+ - curl
+ - vim
+ - bash-completion
+ssh_port: 22
+become: true
+env: production
+vault_enabled: true
+vault_rotation_interval: 30d
diff --git a/core/lib/translate_msg.sh b/core/lib/translate_msg.sh
new file mode 100644
index 0000000..3a1e4a9
--- /dev/null
+++ b/core/lib/translate_msg.sh
@@ -0,0 +1,9 @@
+render_msg() {
+ local template="$1"; shift
+ for pair in "$@"; do
+ local key="${pair%%=*}"
+ local val="${pair#*=}"
+ template="${template//\{$key\}/$val}"
+ done
+ echo "$template"
+}
diff --git a/core/modules/api.sh b/core/modules/api.sh
new file mode 100644
index 0000000..a58f81a
--- /dev/null
+++ b/core/modules/api.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Module: api
+# Description: Cliente declarativo para APIs REST y SOAP (GET, POST, PUT, DELETE, SOAP)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: curl, jq, xmllint
+
+api_task() {
+ local host="$1"; shift
+ declare -A args
+ local headers=()
+ local method="" body="" url="" output="" parse=""
+
+ for arg in "$@"; do
+ key="${arg%%=*}"; value="${arg#*=}"
+ case "$key" in
+ headers) IFS=',' read -r -a headers <<< "$value" ;;
+ body) body="$value" ;;
+ url) url="$value" ;;
+ method) method="${value,,}" ;;
+ output) output="$value" ;;
+ parse) parse="${value,,}" ;;
+ esac
+ done
+
+ [[ -z "$method" ]] && method="get"
+ [[ "$method" == "get" ]] && method="GET"
+ [[ "$method" == "post" ]] && method="POST"
+ [[ "$method" == "soap" ]] && method="POST"
+
+ local header_args=""
+ for h in "${headers[@]}"; do header_args+=" -H \"$h\""; done
+
+ local curl_cmd="curl -sSL -X $method \"$url\"$header_args"
+ [[ -n "$body" ]] && curl_cmd+=" --data-raw '$body'"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ echo "$(render_msg "${tr[start]}" "method=$method" "url=$url")"
+ [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_cmd]}" "cmd=$curl_cmd")"
+ [[ "$DEBUG" == "true" && -n "$body" ]] && echo -e "$(render_msg "${tr[debug_body]}" "body=$body")"
+
+ local response
+ if [[ "$host" == "localhost" ]]; then
+ response=$(eval "$curl_cmd")
+ else
+ response=$(ssh "$host" "$curl_cmd")
+ fi
+
+ if [[ -n "$output" ]]; then
+ echo "$response" > "$output"
+ echo "$(render_msg "${tr[saved]}" "output=$output")"
+ fi
+
+ case "$parse" in
+ json)
+ echo "$response" | jq '.' 2>/dev/null || echo "${tr[json_fail]:-⚠️ [api] No se pudo parsear como JSON}"
+ ;;
+ xml)
+ echo "$response" | xmllint --format - 2>/dev/null || {
+ echo "${tr[xml_fail]:-⚠️ [api] No se pudo parsear como XML}"
+ echo "$response"
+ }
+ ;;
+ *) echo "$response" ;;
+ esac
+}
+
+check_dependencies_api() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ for cmd in curl jq xmllint; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")"
+ else
+ echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")"
+ fi
+ done
+ return 0
+}
diff --git a/core/modules/api.tr.en b/core/modules/api.tr.en
new file mode 100644
index 0000000..fce15f6
--- /dev/null
+++ b/core/modules/api.tr.en
@@ -0,0 +1,8 @@
+start=🌐 [api] Executing {method} → {url}
+debug_cmd=🔍 Actual command: {cmd}
+debug_body=📦 [api] Body sent:\n{body}
+saved=💾 [api] Response saved to: {output}
+json_fail=⚠️ [api] Failed to parse as JSON
+xml_fail=⚠️ [api] Failed to parse as XML
+missing_cmd=⚠️ [api] '{cmd}' not available locally. Assuming it exists on the remote host.
+cmd_ok=✅ [api] '{cmd}' available locally.
diff --git a/core/modules/api.tr.es b/core/modules/api.tr.es
new file mode 100644
index 0000000..6e75c04
--- /dev/null
+++ b/core/modules/api.tr.es
@@ -0,0 +1,8 @@
+start=🌐 [api] Ejecutando {method} → {url}
+debug_cmd=🔍 Comando real: {cmd}
+debug_body=📦 [api] Cuerpo enviado:\n{body}
+saved=💾 [api] Respuesta guardada en: {output}
+json_fail=⚠️ [api] No se pudo parsear como JSON
+xml_fail=⚠️ [api] No se pudo parsear como XML
+missing_cmd=⚠️ [api] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto.
+cmd_ok=✅ [api] '{cmd}' disponible localmente.
diff --git a/core/modules/archive.sh b/core/modules/archive.sh
new file mode 100644
index 0000000..4459bfd
--- /dev/null
+++ b/core/modules/archive.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+# Module: archive
+# Description: Comprime, descomprime y extrae archivos en remoto (tar, zip, gzip, bzip2)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.6.0
+# Dependencies: ssh, tar, gzip, bzip2, zip, unzip
+
+archive_task() {
+ local host="$1"; shift
+ declare -A args
+ local files=()
+
+ for arg in "$@"; do
+ key="${arg%%=*}"; value="${arg#*=}"
+ [[ "$key" == "files" ]] && IFS=',' read -r -a files <<< "$value" || args["$key"]="$value"
+ done
+
+ local action="${args[action]}"
+ local format="${args[format]:-tar}"
+ local become="${args[become]:-false}"
+ local prefix=""
+ [[ "$become" == "true" ]] && prefix="sudo"
+
+ local output="" archive="" dest=""
+ case "$action" in
+ compress) output="${args[output]}" ;;
+ decompress|extract) archive="${args[archive]}"; dest="${args[dest]:-$(dirname "$archive")}" ;;
+ esac
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if [[ "$action" == "extract" || "$action" == "decompress" ]]; then
+ ssh "$host" "[ -d '$dest' ] || $prefix mkdir -p '$dest'" || {
+ echo "$(render_msg "${tr[mkdir_fail]}" "dest=$dest")"
+ return 1
+ }
+ fi
+
+ case "$action" in
+ compress)
+ case "$format" in
+ tar)
+ ssh "$host" "$prefix tar -czf '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_tar]}" "output=$output")"
+ ;;
+ zip)
+ ssh "$host" "$prefix zip -r '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_zip]}" "output=$output")"
+ ;;
+ gzip)
+ for file in "${files[@]}"; do
+ ssh "$host" "$prefix gzip -f '$file'" && echo "$(render_msg "${tr[compressed_gzip]}" "file=$file")"
+ done
+ ;;
+ bzip2)
+ for file in "${files[@]}"; do
+ ssh "$host" "$prefix bzip2 -f '$file'" && echo "$(render_msg "${tr[compressed_bzip2]}" "file=$file")"
+ done
+ ;;
+ *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;;
+ esac
+ ;;
+ decompress)
+ case "$format" in
+ gzip)
+ ssh "$host" "$prefix gunzip -f '$archive'" && echo "$(render_msg "${tr[decompressed_gzip]}" "archive=$archive")"
+ ;;
+ bzip2)
+ ssh "$host" "$prefix bunzip2 -f '$archive'" && echo "$(render_msg "${tr[decompressed_bzip2]}" "archive=$archive")"
+ ;;
+ zip)
+ ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[decompressed_zip]}" "dest=$dest")"
+ ;;
+ *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;;
+ esac
+ ;;
+ extract)
+ case "$format" in
+ tar)
+ if [[ ${#files[@]} -gt 0 ]]; then
+ ssh "$host" "$prefix tar -xzf '$archive' -C '$dest' ${files[*]}" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")"
+ else
+ ssh "$host" "$prefix tar -xzf '$archive' -C '$dest'" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")"
+ fi
+ ;;
+ zip)
+ ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[extracted_zip]}" "dest=$dest")"
+ ;;
+ *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;;
+ esac
+ ;;
+ *) echo "$(render_msg "${tr[unsupported_action]}" "action=$action")"; return 1 ;;
+ esac
+}
+
+check_dependencies_archive() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ for cmd in ssh tar gzip bzip2 zip unzip; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")"
+ else
+ echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")"
+ fi
+ done
+ return 0
+}
diff --git a/core/modules/archive.tr.en b/core/modules/archive.tr.en
new file mode 100644
index 0000000..5528413
--- /dev/null
+++ b/core/modules/archive.tr.en
@@ -0,0 +1,14 @@
+mkdir_fail=❌ [archive] Failed to create destination directory: {dest}
+compressed_tar=📦 [archive] Compressed as TAR: {output}
+compressed_zip=📦 [archive] Compressed as ZIP: {output}
+compressed_gzip=📦 [archive] GZIP: {file}.gz
+compressed_bzip2=📦 [archive] BZIP2: {file}.bz2
+decompressed_gzip=📂 [archive] Decompressed GZIP: {archive}
+decompressed_bzip2=📂 [archive] Decompressed BZIP2: {archive}
+decompressed_zip=📂 [archive] Decompressed ZIP into: {dest}
+extracted_tar=📂 [archive] Extracted TAR into: {dest}
+extracted_zip=📂 [archive] Extracted ZIP into: {dest}
+unsupported_format=❌ [archive] Unsupported format '{format}'
+unsupported_action=❌ [archive] Unsupported action '{action}'
+missing_cmd=⚠️ [archive] '{cmd}' not available locally. Assuming it exists on the remote host.
+cmd_ok=✅ [archive] '{cmd}' available locally.
diff --git a/core/modules/archive.tr.es b/core/modules/archive.tr.es
new file mode 100644
index 0000000..86764fd
--- /dev/null
+++ b/core/modules/archive.tr.es
@@ -0,0 +1,14 @@
+mkdir_fail=❌ [archive] No se pudo crear el directorio destino: {dest}
+compressed_tar=📦 [archive] Comprimido en TAR: {output}
+compressed_zip=📦 [archive] Comprimido en ZIP: {output}
+compressed_gzip=📦 [archive] GZIP: {file}.gz
+compressed_bzip2=📦 [archive] BZIP2: {file}.bz2
+decompressed_gzip=📂 [archive] Descomprimido GZIP: {archive}
+decompressed_bzip2=📂 [archive] Descomprimido BZIP2: {archive}
+decompressed_zip=📂 [archive] Descomprimido ZIP en: {dest}
+extracted_tar=📂 [archive] Extraído TAR en: {dest}
+extracted_zip=📂 [archive] Extraído ZIP en: {dest}
+unsupported_format=❌ [archive] Formato '{format}' no soportado
+unsupported_action=❌ [archive] Acción '{action}' no soportada
+missing_cmd=⚠️ [archive] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto.
+cmd_ok=✅ [archive] '{cmd}' disponible localmente.
diff --git a/core/modules/blockinfile.sh b/core/modules/blockinfile.sh
new file mode 100644
index 0000000..e42f5d8
--- /dev/null
+++ b/core/modules/blockinfile.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+# Module: blockinfile
+# Description: Inserta o actualiza bloques de texto delimitados en archivos
+# Author: Luis GuLo
+# Version: 0.2.0
+# Dependencies: grep, sed, tee, awk
+
+blockinfile_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"
+ done
+
+ local path="${args[path]}"
+ local block="${args[block]}"
+ local marker="${args[marker]:-SHFLOW}"
+ local create="${args[create]:-true}"
+ local backup="${args[backup]:-true}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ local start="# BEGIN $marker"
+ local end="# END $marker"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if [[ ! -f "$path" ]]; then
+ if [[ "$create" == "true" ]]; then
+ echo "$(render_msg "${tr[creating]}" "path=$path")"
+ touch "$path"
+ else
+ echo "$(render_msg "${tr[missing_file]}" "path=$path")"
+ return 1
+ fi
+ fi
+
+ if [[ "$backup" == "true" ]]; then
+ cp "$path" "$path.bak"
+ echo "$(render_msg "${tr[backup]}" "path=$path")"
+ fi
+
+ if grep -q "$start" "$path"; then
+ echo "$(render_msg "${tr[replacing]}" "marker=$marker")"
+ $prefix sed -i "/$start/,/$end/d" "$path"
+ fi
+
+ echo "$(render_msg "${tr[inserting]}" "marker=$marker")"
+ {
+ echo "$start"
+ echo "$block"
+ echo "$end"
+ } | $prefix tee -a "$path" > /dev/null
+}
+
+check_dependencies_blockinfile() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ local missing=()
+ for cmd in grep sed tee awk; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [blockinfile] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/blockinfile.tr.en b/core/modules/blockinfile.tr.en
new file mode 100644
index 0000000..70a7711
--- /dev/null
+++ b/core/modules/blockinfile.tr.en
@@ -0,0 +1,7 @@
+creating=📄 [blockinfile] Creating file: {path}
+missing_file=❌ [blockinfile] File does not exist and create=false
+backup=📦 Backup created: {path}.bak
+replacing=🔁 [blockinfile] Replacing existing block with marker '{marker}'
+inserting=➕ [blockinfile] Inserting delimited block with marker '{marker}'
+missing_deps=❌ [blockinfile] Missing dependencies: {cmds}
+deps_ok=✅ [blockinfile] All dependencies are available
diff --git a/core/modules/blockinfile.tr.es b/core/modules/blockinfile.tr.es
new file mode 100644
index 0000000..98df5c7
--- /dev/null
+++ b/core/modules/blockinfile.tr.es
@@ -0,0 +1,7 @@
+creating=📄 [blockinfile] Creando archivo: {path}
+missing_file=❌ [blockinfile] El archivo no existe y create=false
+backup=📦 Copia de seguridad creada: {path}.bak
+replacing=🔁 [blockinfile] Reemplazando bloque existente con marcador '{marker}'
+inserting=➕ [blockinfile] Insertando bloque delimitado con marcador '{marker}'
+missing_deps=❌ [blockinfile] Dependencias faltantes: {cmds}
+deps_ok=✅ [blockinfile] Todas las dependencias están disponibles
diff --git a/core/modules/copy.sh b/core/modules/copy.sh
new file mode 100644
index 0000000..b46d9e5
--- /dev/null
+++ b/core/modules/copy.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+# Module: copy
+# Description: Copia archivos locales al host remoto usando scp
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: scp, ssh
+
+copy_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local src="${args[src]}"
+ local dest="${args[dest]}"
+ local mode="${args[mode]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if [[ -z "$src" || -z "$dest" ]]; then
+ echo "${tr[missing_args]:-❌ [copy] Faltan parámetros: src y dest son obligatorios}"
+ return 1
+ fi
+
+ echo "$(render_msg "${tr[copying]}" "src=$src" "host=$host")"
+ scp "$src" "$host:/tmp/shflow_tmpfile" || {
+ echo "$(render_msg "${tr[scp_fail]}" "src=$src" "host=$host")"
+ return 1
+ }
+
+ echo "$(render_msg "${tr[moving]}" "dest=$dest")"
+ ssh "$host" "$prefix mv /tmp/shflow_tmpfile '$dest' && $prefix chmod $mode '$dest'" && \
+ echo "$(render_msg "${tr[done]}" "dest=$dest")"
+}
+
+check_dependencies_copy() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ local missing=()
+ for cmd in scp ssh; do
+ command -v "$cmd" &> /dev/null || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [copy] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/copy.tr.en b/core/modules/copy.tr.en
new file mode 100644
index 0000000..dcace81
--- /dev/null
+++ b/core/modules/copy.tr.en
@@ -0,0 +1,7 @@
+missing_args=❌ [copy] Missing parameters: src and dest are required
+copying=📤 [copy] Copying '{src}' to host '{host}'...
+scp_fail=❌ [copy] Failed to copy '{src}' to host '{host}'
+moving=📦 [copy] Moving file to final destination: '{dest}'
+done=✅ [copy] File successfully installed at '{dest}'
+missing_deps=❌ [copy] Missing dependencies: {cmds}
+deps_ok=✅ [copy] All dependencies are available
diff --git a/core/modules/copy.tr.es b/core/modules/copy.tr.es
new file mode 100644
index 0000000..9b5f934
--- /dev/null
+++ b/core/modules/copy.tr.es
@@ -0,0 +1,7 @@
+missing_args=❌ [copy] Faltan parámetros: src y dest son obligatorios
+copying=📤 [copy] Copiando '{src}' al host '{host}'...
+scp_fail=❌ [copy] Falló la copia de '{src}' al host '{host}'
+moving=📦 [copy] Moviendo archivo a destino final: '{dest}'
+done=✅ [copy] Archivo instalado correctamente en '{dest}'
+missing_deps=❌ [copy] Dependencias faltantes: {cmds}
+deps_ok=✅ [copy] Todas las dependencias están disponibles
diff --git a/core/modules/cron.sh b/core/modules/cron.sh
new file mode 100644
index 0000000..c1c4533
--- /dev/null
+++ b/core/modules/cron.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+# Module: cron
+# Description: Gestiona entradas de cron para usuarios del sistema (crear, modificar, eliminar, listar)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: bash, crontab, grep, id, sudo
+
+cron_task() {
+ local host="$1"; shift
+ local alias="" user="" state="" schedule="" command=""
+ for arg in "$@"; do
+ case "$arg" in
+ alias=*) alias="${arg#alias=}" ;;
+ user=*) user="${arg#user=}" ;;
+ state=*) state="${arg#state=}" ;;
+ schedule=*) schedule="${arg#schedule=}" ;;
+ command=*) command="${arg#command=}" ;;
+ esac
+ done
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if [[ -z "$user" || -z "$state" ]]; then
+ echo "${tr[missing_args]:-❌ [cron] Faltan argumentos obligatorios: 'user' y 'state'}"
+ return 1
+ fi
+
+ if ! id "$user" &>/dev/null; then
+ echo "$(render_msg "${tr[user_not_found]}" "user=$user")"
+ return 1
+ fi
+
+ local tag="# shflow:$alias"
+ local tmpfile
+ tmpfile=$(mktemp)
+
+ echo "$(render_msg "${tr[checking]}" "user=$user")"
+ sudo crontab -u "$user" -l 2>/dev/null > "$tmpfile" || true
+
+ case "$state" in
+ list)
+ echo "$(render_msg "${tr[list]}" "user=$user")"
+ grep -E "^.*$tag|^[^#]" "$tmpfile" || echo "${tr[no_entries]:-⚠️ [cron] No hay entradas visibles}"
+ rm -f "$tmpfile"
+ return 0
+ ;;
+ absent)
+ if grep -q "$tag" "$tmpfile"; then
+ grep -v "$tag" "$tmpfile" > "${tmpfile}.new"
+ sudo crontab -u "$user" "${tmpfile}.new"
+ echo "$(render_msg "${tr[removed]}" "alias=$alias")"
+ rm -f "${tmpfile}.new"
+ else
+ echo "$(render_msg "${tr[not_found]}" "alias=$alias")"
+ fi
+ rm -f "$tmpfile"
+ return 0
+ ;;
+ present)
+ if [[ -z "$alias" || -z "$schedule" || -z "$command" ]]; then
+ echo "${tr[missing_present]:-❌ [cron] Para 'present' se requieren: alias, schedule y command}"
+ rm -f "$tmpfile"
+ return 1
+ fi
+ grep -v "$tag" "$tmpfile" > "${tmpfile}.new"
+ echo "$schedule $command $tag" >> "${tmpfile}.new"
+ sudo crontab -u "$user" "${tmpfile}.new"
+ echo "$(render_msg "${tr[added]}" "alias=$alias")"
+ rm -f "$tmpfile" "${tmpfile}.new"
+ return 0
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "state=$state")"
+ rm -f "$tmpfile"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_cron() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if ! command -v sudo &>/dev/null; then
+ echo "${tr[missing_sudo]:-❌ [cron] El comando 'sudo' no está disponible}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [cron] Dependencias OK}"
+ return 0
+}
diff --git a/core/modules/cron.tr.en b/core/modules/cron.tr.en
new file mode 100644
index 0000000..404c5f0
--- /dev/null
+++ b/core/modules/cron.tr.en
@@ -0,0 +1,12 @@
+missing_args=❌ [cron] Missing required arguments: 'user' and 'state'
+user_not_found=❌ [cron] User '{user}' does not exist on the system
+checking=🕒 [cron] Checking cron entries for user '{user}'...
+list=📋 [cron] Current entries for '{user}':
+no_entries=⚠️ [cron] No visible entries
+removed=➖ [cron] Entry '{alias}' successfully removed
+not_found=⚠️ [cron] Entry '{alias}' not found, nothing removed
+missing_present=❌ [cron] For 'present', alias, schedule and command are required
+added=➕ [cron] Entry '{alias}' successfully created/modified
+unsupported=❌ [cron] Unknown state: '{state}'. Use 'present', 'absent' or 'list'
+missing_sudo=❌ [cron] 'sudo' command is not available
+deps_ok=✅ [cron] Dependencies OK
diff --git a/core/modules/cron.tr.es b/core/modules/cron.tr.es
new file mode 100644
index 0000000..683e2f8
--- /dev/null
+++ b/core/modules/cron.tr.es
@@ -0,0 +1,12 @@
+missing_args=❌ [cron] Faltan argumentos obligatorios: 'user' y 'state'
+user_not_found=❌ [cron] El usuario '{user}' no existe en el sistema
+checking=🕒 [cron] Revisando entradas de cron para usuario '{user}'...
+list=📋 [cron] Entradas actuales para '{user}':
+no_entries=⚠️ [cron] No hay entradas visibles
+removed=➖ [cron] Entrada '{alias}' eliminada correctamente
+not_found=⚠️ [cron] Entrada '{alias}' no encontrada, no se eliminó nada
+missing_present=❌ [cron] Para 'present' se requieren: alias, schedule y command
+added=➕ [cron] Entrada '{alias}' creada/modificada correctamente
+unsupported=❌ [cron] Estado desconocido: '{state}'. Usa 'present', 'absent' o 'list'
+missing_sudo=❌ [cron] El comando 'sudo' no está disponible
+deps_ok=✅ [cron] Dependencias OK
diff --git a/core/modules/docker.sh b/core/modules/docker.sh
new file mode 100644
index 0000000..9d3350e
--- /dev/null
+++ b/core/modules/docker.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+# License: GPLv3
+# Module: docker
+# Description: Gestiona contenedores Docker (run, stop, remove, build, exec)
+# Author: Luis GuLo
+# Version: 1.7.0
+# Dependencies: ssh, docker
+
+docker_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local action="${args[action]}"
+ local become="${args[become]:-false}"
+ local detach="${args[detach]:-true}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+ local detached="-d"
+ [ "$detach" = "false" ] && detached=""
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ local name="" image="" path="" command=""
+ case "$action" in present|stopped|absent|exec) name="${args[name]}" ;; esac
+ case "$action" in present|build) image="${args[image]}" ;; esac
+ [[ "$action" == "build" ]] && path="${args[path]}"
+ [[ "$action" == "exec" ]] && command="${args[command]}"
+
+ case "$action" in
+ present)
+ local extra="${args[run_args]:-${args[extra_args]:-}}"
+ echo "$(render_msg "${tr[run]}" "name=$name" "image=$image")"
+ ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' || $prefix docker run $detached --name '$name' $extra '$image'"
+ ;;
+ stopped)
+ echo "$(render_msg "${tr[stop]}" "name=$name")"
+ ssh "$host" "$prefix docker ps --format '{{.Names}}' | grep -q '^$name$' && $prefix docker stop '$name'"
+ ;;
+ absent)
+ echo "$(render_msg "${tr[remove]}" "name=$name")"
+ ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' && $prefix docker rm -f '$name'"
+ ;;
+ build)
+ echo "$(render_msg "${tr[build]}" "image=$image" "path=$path")"
+ ssh "$host" "cd '$path' && $prefix docker build -t '$image' ."
+ ;;
+ exec)
+ echo "$(render_msg "${tr[exec]}" "name=$name" "command=$command")"
+ ssh "$host" "$prefix docker exec '$name' $command"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "action=$action")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_docker() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_ssh]:-❌ [docker] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[ssh_ok]:-✅ [docker] ssh disponible.}"
+
+ if ! command -v docker &> /dev/null; then
+ echo "${tr[missing_docker]:-⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto.}"
+ else
+ echo "${tr[docker_ok]:-✅ [docker] docker disponible localmente.}"
+ fi
+ return 0
+}
diff --git a/core/modules/docker.tr.en b/core/modules/docker.tr.en
new file mode 100644
index 0000000..59844f2
--- /dev/null
+++ b/core/modules/docker.tr.en
@@ -0,0 +1,10 @@
+run=🧪 [docker] Running container '{name}' with image '{image}'
+stop=🛑 [docker] Stopping container '{name}'
+remove=🧹 [docker] Removing container '{name}'
+build=🏗️ [docker] Building image '{image}' from '{path}'
+exec=🚀 [docker] Executing command in '{name}': {command}
+unsupported=❌ [docker] Unsupported action '{action}'
+missing_ssh=❌ [docker] ssh is not available.
+ssh_ok=✅ [docker] ssh is available.
+missing_docker=⚠️ [docker] docker not available locally. Assuming it exists on the remote host.
+docker_ok=✅ [docker] docker available locally.
diff --git a/core/modules/docker.tr.es b/core/modules/docker.tr.es
new file mode 100644
index 0000000..4abe373
--- /dev/null
+++ b/core/modules/docker.tr.es
@@ -0,0 +1,10 @@
+run=🧪 [docker] Ejecutando contenedor '{name}' con imagen '{image}'
+stop=🛑 [docker] Deteniendo contenedor '{name}'
+remove=🧹 [docker] Eliminando contenedor '{name}'
+build=🏗️ [docker] Construyendo imagen '{image}' desde '{path}'
+exec=🚀 [docker] Ejecutando comando en '{name}': {command}
+unsupported=❌ [docker] Acción '{action}' no soportada
+missing_ssh=❌ [docker] ssh no está disponible.
+ssh_ok=✅ [docker] ssh disponible.
+missing_docker=⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto.
+docker_ok=✅ [docker] docker disponible localmente.
diff --git a/core/modules/download.sh b/core/modules/download.sh
new file mode 100644
index 0000000..a71fcc4
--- /dev/null
+++ b/core/modules/download.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# Module: download
+# Description: Descarga ficheros remotos con soporte para reintentos, proxy y reanudación
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: wget o curl, sudo (si become=true)
+
+download_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local url="${args[url]}"
+ local dest="${args[dest]:-$(basename "$url")}"
+ local proxy="${args[proxy]:-}"
+ local continue="${args[continue]:-true}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if [[ -z "$url" ]]; then
+ echo "${tr[missing_url]:-❌ [download] Falta el parámetro obligatorio 'url'}"
+ return 1
+ fi
+
+ local cmd=""
+ if command -v wget &>/dev/null; then
+ echo "${tr[using_wget]:-📦 [download] Usando wget}"
+ cmd="$prefix wget \"$url\" -O \"$dest\""
+ [[ "$continue" == "true" ]] && cmd="$cmd -c"
+ [[ -n "$proxy" ]] && cmd="$cmd -e use_proxy=yes -e http_proxy=\"$proxy\""
+ elif command -v curl &>/dev/null; then
+ echo "${tr[using_curl]:-📦 [download] Usando curl}"
+ cmd="$prefix curl -L \"$url\" -o \"$dest\""
+ [[ "$continue" == "true" ]] && cmd="$cmd -C -"
+ [[ -n "$proxy" ]] && cmd="$cmd --proxy \"$proxy\""
+ else
+ echo "${tr[missing_tool]:-❌ [download] Ni wget ni curl están disponibles}"
+ return 1
+ fi
+
+ echo "$(render_msg "${tr[start]}" "url=$url" "dest=$dest")"
+ eval "$cmd" && echo "$(render_msg "${tr[done]}" "dest=$dest")"
+}
+
+check_dependencies_download() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ if ! command -v wget &>/dev/null && ! command -v curl &>/dev/null; then
+ echo "${tr[missing_tool]:-❌ [download] Se requiere 'wget' o 'curl'}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [download] Herramienta de descarga disponible}"
+ return 0
+}
diff --git a/core/modules/download.tr.en b/core/modules/download.tr.en
new file mode 100644
index 0000000..04526c4
--- /dev/null
+++ b/core/modules/download.tr.en
@@ -0,0 +1,7 @@
+missing_url=❌ [download] Missing required parameter 'url'
+using_wget=📦 [download] Using wget
+using_curl=📦 [download] Using curl
+missing_tool=❌ [download] Neither wget nor curl are available
+start=🔧 [download] Downloading '{url}' → '{dest}'
+done=✅ [download] Download completed: {dest}
+deps_ok=✅ [download] Download tool available
diff --git a/core/modules/download.tr.es b/core/modules/download.tr.es
new file mode 100644
index 0000000..d4f54d3
--- /dev/null
+++ b/core/modules/download.tr.es
@@ -0,0 +1,7 @@
+missing_url=❌ [download] Falta el parámetro obligatorio 'url'
+using_wget=📦 [download] Usando wget
+using_curl=📦 [download] Usando curl
+missing_tool=❌ [download] Ni wget ni curl están disponibles
+start=🔧 [download] Descargando '{url}' → '{dest}'
+done=✅ [download] Descarga completada: {dest}
+deps_ok=✅ [download] Herramienta de descarga disponible
diff --git a/core/modules/echo.sh b/core/modules/echo.sh
new file mode 100644
index 0000000..8b562a5
--- /dev/null
+++ b/core/modules/echo.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+# Module: echo
+# Description: Muestra un mensaje en consola con soporte para variables ShFlow
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: -
+
+echo_task() {
+ local host="$1"; shift
+ declare -A args
+
+ while [[ "$#" -gt 0 ]]; do
+ case "$1" in
+ *=*)
+ key="${1%%=*}"
+ value="${1#*=}"
+ args["$key"]="$value"
+ ;;
+ esac
+ shift
+ done
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # 🔁 Interpolar usando argumentos explícitos
+ for key in "${!args[@]}"; do
+ for var in "${!args[@]}"; do
+ args["$key"]="${args[$key]//\{\{ $var \}\}/${args[$var]}}"
+ done
+ done
+
+ local message="${args[message]}"
+ echo "$(render_msg "${tr[output]}" "message=$message")"
+}
+
+check_dependencies_echo() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ echo "${tr[deps_ok]:-✅ [echo] No requiere dependencias.}"
+ return 0
+}
diff --git a/core/modules/echo.tr.en b/core/modules/echo.tr.en
new file mode 100644
index 0000000..ab2154a
--- /dev/null
+++ b/core/modules/echo.tr.en
@@ -0,0 +1,2 @@
+output=📣 [echo] {message}
+deps_ok=✅ [echo] No dependencies required.
diff --git a/core/modules/echo.tr.es b/core/modules/echo.tr.es
new file mode 100644
index 0000000..ae12ef8
--- /dev/null
+++ b/core/modules/echo.tr.es
@@ -0,0 +1,2 @@
+output=📣 [echo] {message}
+deps_ok=✅ [echo] No requiere dependencias.
diff --git a/core/modules/facts.sh b/core/modules/facts.sh
new file mode 100644
index 0000000..498ccd0
--- /dev/null
+++ b/core/modules/facts.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+# Module: facts
+# Description: Extrae información del sistema con opciones de formato, filtrado y salida
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.4.0
+# Dependencies: lscpu, ip, free, lsblk, uname, hostnamectl
+
+facts_task() {
+ local host="$1"; shift
+ declare -A args
+ local field="" format="plain" output="" append="false" host_label=""
+
+ for arg in "$@"; do
+ key="${arg%%=*}"; value="${arg#*=}"
+ case "$key" in
+ field) field="$value" ;;
+ format) format="${value,,}" ;;
+ output) output="$value" ;;
+ append) append="$value" ;;
+ host_label) host_label="$value" ;;
+ esac
+ done
+
+ [[ -z "$host_label" ]] && host_label="$host"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local prefix=""
+ [[ "$host" != "localhost" ]] && prefix="ssh $host"
+
+ [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_prefix]}" "prefix=$prefix")"
+
+ local raw
+ raw=$($prefix bash --noprofile --norc <<'EOF'
+ cd /tmp || cd ~
+ echo "hostname=$(hostname)"
+ lscpu | awk '/^CPU\(s\):/ {print "cpu_count="$2}'
+ free -m | awk '/Mem:/ {print "ram_total_mb="$2}'
+ if command -v hostnamectl &> /dev/null; then
+ hostnamectl | awk -F: '/Operating System/ {print "os_name=" $2}' | sed 's/^ *//'
+ hostnamectl | awk -F: '/Kernel/ {print "os_version=" $2}' | sed 's/^ *//'
+ else
+ echo "os_name=$(uname -s)"
+ echo "os_version=$(uname -r)"
+ fi
+ ip link show | awk -F: '/^[0-9]+: / {print $2}' | grep -Ev 'docker|virbr|lo|veth|br-' | while read -r dev; do
+ ip=$(ip -4 addr show "$dev" | awk '/inet / {print $2}' | cut -d/ -f1)
+ mac=$(ip link show "$dev" | awk '/ether/ {print $2}')
+ [[ -n "$ip" || -n "$mac" ]] && echo "net_$dev=IP:$ip MAC:$mac"
+ done
+ ip -4 addr show | awk '/inet / {print $2}' | cut -d/ -f1 | paste -sd ' ' - | awk '{print "ip_addresses="$0}'
+ lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT | grep -Ev 'loop|tmpfs|overlay|docker' | awk 'NR>1 && NF>0 {print "partition_list=" $1 " " $2 " " $3 " " $4}'
+EOF
+)
+
+ [[ -n "$field" ]] && raw=$(echo "$raw" | grep "^$field=")
+
+ local partitions=() facts=()
+ while IFS= read -r line; do
+ [[ "$line" == partition_list=* ]] && partitions+=("${line#*=}") || facts+=("$line")
+ done <<< "$raw"
+
+ local formatted=""
+ case "$format" in
+ plain)
+ formatted+="Host: $host_label\n"
+ for f in "${facts[@]}"; do formatted+="${f%%=*}: ${f#*=}\n"; done
+ [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions:\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done
+ ;;
+ md)
+ formatted+="### $host_label\n"
+ for f in "${facts[@]}"; do formatted+="- **${f%%=*}:** ${f#*=}\n"; done
+ [[ ${#partitions[@]} -gt 0 ]] && formatted+="- **partitions:**\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done
+ ;;
+ kv)
+ for f in "${facts[@]}"; do formatted+="$f\n"; done
+ [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions=$(IFS=';'; echo "${partitions[*]}")\n"
+ ;;
+ json)
+ local json="{"
+ for f in "${facts[@]}"; do json+="\"${f%%=*}\":\"${f#*=}\","; done
+ [[ ${#partitions[@]} -gt 0 ]] && json+="\"partitions\":[" && for p in "${partitions[@]}"; do json+="\"$p\","; done && json="${json%,}]" || json="${json%,}"
+ json+="}"
+ formatted="$json"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"
+ return 1
+ ;;
+ esac
+
+ if [[ -n "$output" ]]; then
+ [[ "$append" == "true" ]] && echo -e "$formatted" >> "$output" || echo -e "$formatted" > "$output"
+ echo "$(render_msg "${tr[saved]}" "output=$output")"
+ else
+ echo -e "$formatted"
+ fi
+}
+
+check_dependencies_facts() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+ for cmd in lscpu ip free lsblk uname hostnamectl; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")"
+ else
+ echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")"
+ fi
+ done
+ return 0
+}
diff --git a/core/modules/facts.tr.en b/core/modules/facts.tr.en
new file mode 100644
index 0000000..015ef5b
--- /dev/null
+++ b/core/modules/facts.tr.en
@@ -0,0 +1,5 @@
+debug_prefix=🔍 SSH line: {prefix}
+unsupported_format=❌ [facts] Format '{format}' not supported.
+saved=💾 [facts] Report saved to: {output}
+missing_cmd=⚠️ [facts] '{cmd}' not available locally. Assuming it exists on remote host.
+cmd_ok=✅ [facts] '{cmd}' available locally.
diff --git a/core/modules/facts.tr.es b/core/modules/facts.tr.es
new file mode 100644
index 0000000..7d2b08b
--- /dev/null
+++ b/core/modules/facts.tr.es
@@ -0,0 +1,5 @@
+debug_prefix=🔍 Línea SSH: {prefix}
+unsupported_format=❌ [facts] Formato '{format}' no soportado.
+saved=💾 [facts] Informe guardado en: {output}
+missing_cmd=⚠️ [facts] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto.
+cmd_ok=✅ [facts] '{cmd}' disponible localmente.
diff --git a/core/modules/file.sh b/core/modules/file.sh
new file mode 100644
index 0000000..44804af
--- /dev/null
+++ b/core/modules/file.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# Module: file
+# Description: Gestiona archivos y directorios remotos (crear, eliminar, permisos)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: ssh
+
+file_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local path="${args[path]}"
+ local state="${args[state]}"
+ local type="${args[type]}"
+ local mode="${args[mode]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ case "$state" in
+ present)
+ if [[ "$type" == "directory" ]]; then
+ echo "$(render_msg "${tr[creating_dir]}" "path=$path")"
+ ssh "$host" "[ -d '$path' ] || $prefix mkdir -p '$path'"
+ elif [[ "$type" == "file" ]]; then
+ echo "$(render_msg "${tr[creating_file]}" "path=$path")"
+ ssh "$host" "[ -f '$path' ] || $prefix touch '$path'"
+ fi
+ if [[ -n "$mode" ]]; then
+ echo "$(render_msg "${tr[setting_mode]}" "mode=$mode" "path=$path")"
+ ssh "$host" "$prefix chmod $mode '$path'"
+ fi
+ ;;
+ absent)
+ if [[ "$type" == "directory" ]]; then
+ echo "$(render_msg "${tr[removing_dir]}" "path=$path")"
+ ssh "$host" "[ -d '$path' ] && $prefix rm -rf '$path'"
+ elif [[ "$type" == "file" ]]; then
+ echo "$(render_msg "${tr[removing_file]}" "path=$path")"
+ ssh "$host" "[ -f '$path' ] && $prefix rm -f '$path'"
+ fi
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_state]}" "state=$state")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_file() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [file] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [file] ssh disponible.}"
+ return 0
+}
diff --git a/core/modules/file.tr.en b/core/modules/file.tr.en
new file mode 100644
index 0000000..2d11c91
--- /dev/null
+++ b/core/modules/file.tr.en
@@ -0,0 +1,8 @@
+creating_dir=📁 [file] Creating directory: {path}
+creating_file=📄 [file] Creating file: {path}
+setting_mode=🔐 [file] Setting permissions {mode} on {path}
+removing_dir=🧹 [file] Removing directory: {path}
+removing_file=🧹 [file] Removing file: {path}
+unsupported_state=❌ [file] Unsupported state '{state}'. Use present or absent.
+missing_deps=❌ [file] ssh is not available.
+deps_ok=✅ [file] ssh is available.
diff --git a/core/modules/file.tr.es b/core/modules/file.tr.es
new file mode 100644
index 0000000..4123cad
--- /dev/null
+++ b/core/modules/file.tr.es
@@ -0,0 +1,8 @@
+creating_dir=📁 [file] Creando directorio: {path}
+creating_file=📄 [file] Creando archivo: {path}
+setting_mode=🔐 [file] Estableciendo permisos {mode} en {path}
+removing_dir=🧹 [file] Eliminando directorio: {path}
+removing_file=🧹 [file] Eliminando archivo: {path}
+unsupported_state=❌ [file] Estado '{state}' no soportado. Usa present o absent.
+missing_deps=❌ [file] ssh no está disponible.
+deps_ok=✅ [file] ssh disponible.
diff --git a/core/modules/file_read.sh b/core/modules/file_read.sh
new file mode 100644
index 0000000..e21423f
--- /dev/null
+++ b/core/modules/file_read.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+# Module: file_read
+# Description: Lee el contenido de un archivo remoto, con opción de filtrado por patrón
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: ssh, cat, grep
+
+file_read_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local path="${args[path]}"
+ local grep="${args[grep]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if [[ -z "$path" ]]; then
+ echo "${tr[missing_path]:-❌ [file_read] Parámetro 'path' obligatorio}"
+ return 1
+ fi
+
+ echo "$(render_msg "${tr[start]}" "path=$path" "host=$host")"
+
+ if [[ -n "$grep" ]]; then
+ ssh "$host" "$prefix grep -E '$grep' '$path'"
+ else
+ ssh "$host" "$prefix cat '$path'"
+ fi
+}
+
+check_dependencies_file_read() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null || ! command -v grep &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [file_read] ssh o grep no están disponibles.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [file_read] ssh y grep disponibles.}"
+ return 0
+}
diff --git a/core/modules/file_read.tr.en b/core/modules/file_read.tr.en
new file mode 100644
index 0000000..117764e
--- /dev/null
+++ b/core/modules/file_read.tr.en
@@ -0,0 +1,4 @@
+missing_path=❌ [file_read] Parameter 'path' is required
+start=📄 [file_read] Reading file '{path}' on {host}...
+missing_deps=❌ [file_read] ssh or grep are not available.
+deps_ok=✅ [file_read] ssh and grep are available.
diff --git a/core/modules/file_read.tr.es b/core/modules/file_read.tr.es
new file mode 100644
index 0000000..0331aa7
--- /dev/null
+++ b/core/modules/file_read.tr.es
@@ -0,0 +1,4 @@
+missing_path=❌ [file_read] Parámetro 'path' obligatorio
+start=📄 [file_read] Leyendo archivo '{path}' en {host}...
+missing_deps=❌ [file_read] ssh o grep no están disponibles.
+deps_ok=✅ [file_read] ssh y grep disponibles.
diff --git a/core/modules/fs.sh b/core/modules/fs.sh
new file mode 100644
index 0000000..1b16bad
--- /dev/null
+++ b/core/modules/fs.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env bash
+# Module: fs
+# Description: Operaciones remotas sobre ficheros (mover, renombrar, copiar, borrar, truncar)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.3.0
+# Dependencies: ssh
+
+fs_task() {
+ local host="$1"; shift
+ declare -A args
+ local files=()
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # Parseo de argumentos
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ if [[ "$key" == "files" ]]; then
+ if [[ "$value" == *'*'* || "$value" == *'?'* || "$value" == *'['* ]]; then
+ mapfile -t files < <(ssh "$host" "ls -1 $value 2>/dev/null")
+ else
+ IFS=',' read -r -a files <<< "$value"
+ fi
+ else
+ args["$key"]="$value"
+ fi
+ done
+
+ local action="${args[action]}"
+ local src="${args[src]}"
+ local dest="${args[dest]}"
+ local path="${args[path]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ case "$action" in
+ move|rename|copy)
+ if [[ ${#files[@]} -gt 0 ]]; then
+ for file in "${files[@]}"; do
+ base="$(basename "$file")"
+ target="$dest/$base"
+ cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp"
+ ssh "$host" "$cmd '$file' '$target'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file" "dest=$target")"
+ done
+ else
+ cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp"
+ ssh "$host" "$cmd '$src' '$dest'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$src" "dest=$dest")"
+ fi
+ ;;
+ delete|truncate)
+ if [[ ${#files[@]} -gt 0 ]]; then
+ for file in "${files[@]}"; do
+ cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0"
+ ssh "$host" "$cmd '$file'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file")"
+ done
+ else
+ cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0"
+ ssh "$host" "$cmd '$path'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$path")"
+ fi
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "action=$action")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_fs() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [fs] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [fs] ssh disponible.}"
+ return 0
+}
diff --git a/core/modules/fs.tr.en b/core/modules/fs.tr.en
new file mode 100644
index 0000000..4e0b2ef
--- /dev/null
+++ b/core/modules/fs.tr.en
@@ -0,0 +1,4 @@
+action_ok=📁 [{action}] {src} → {dest}
+unsupported=❌ [fs] Unsupported action '{action}'.
+missing_deps=❌ [fs] ssh is not available.
+deps_ok=✅ [fs] ssh is available.
diff --git a/core/modules/fs.tr.es b/core/modules/fs.tr.es
new file mode 100644
index 0000000..35f3e0a
--- /dev/null
+++ b/core/modules/fs.tr.es
@@ -0,0 +1,4 @@
+action_ok=📁 [{action}] {src} → {dest}
+unsupported=❌ [fs] Acción '{action}' no soportada.
+missing_deps=❌ [fs] ssh no está disponible.
+deps_ok=✅ [fs] ssh disponible.
diff --git a/core/modules/git.sh b/core/modules/git.sh
new file mode 100644
index 0000000..0fa22f1
--- /dev/null
+++ b/core/modules/git.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+# Module: git
+# Description: Gestiona repositorios Git en hosts remotos (clone, pull, checkout, fetch-file)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: ssh, git, curl, tar
+
+git_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local action="${args[action]}"
+ local repo="${args[repo]}"
+ local dest="${args[dest]}"
+ local branch="${args[branch]}"
+ local file_path="${args[file_path]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ case "$action" in
+ clone)
+ echo "$(render_msg "${tr[cloning]}" "repo=$repo" "dest=$dest")"
+ ssh "$host" "[ -d '$dest/.git' ] || $prefix git clone '$repo' '$dest'"
+ ;;
+ pull)
+ echo "$(render_msg "${tr[pulling]}" "dest=$dest")"
+ ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git pull"
+ ;;
+ checkout)
+ echo "$(render_msg "${tr[checkout]}" "branch=$branch" "dest=$dest")"
+ ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git checkout '$branch'"
+ ;;
+ fetch-file)
+ echo "$(render_msg "${tr[fetching]}" "file=$file_path" "repo=$repo" "branch=$branch")"
+ fetch_file_from_repo "$host" "$repo" "$branch" "$file_path" "$dest" "$become"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "action=$action")"
+ return 1
+ ;;
+ esac
+}
+
+fetch_file_from_repo() {
+ local host="$1"
+ local repo="$2"
+ local branch="$3"
+ local file_path="$4"
+ local dest="$5"
+ local become="$6"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ ssh "$host" "$prefix git archive --remote='$repo' '$branch' '$file_path' | $prefix tar -xO > '$dest'"
+}
+
+check_dependencies_git() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in ssh git curl tar; do
+ command -v "$cmd" &> /dev/null || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [git] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/git.tr.en b/core/modules/git.tr.en
new file mode 100644
index 0000000..ca27acd
--- /dev/null
+++ b/core/modules/git.tr.en
@@ -0,0 +1,7 @@
+cloning=📥 [git] Cloning repository '{repo}' into '{dest}'
+pulling=🔄 [git] Running git pull in '{dest}'
+checkout=📦 [git] Switching to branch '{branch}' in '{dest}'
+fetching=📄 [git] Fetching file '{file}' from '{repo}' branch '{branch}'
+unsupported=❌ [git] Unsupported action '{action}'
+missing_deps=❌ [git] Missing dependencies: {cmds}
+deps_ok=✅ [git] All dependencies are available
diff --git a/core/modules/git.tr.es b/core/modules/git.tr.es
new file mode 100644
index 0000000..60a68f7
--- /dev/null
+++ b/core/modules/git.tr.es
@@ -0,0 +1,7 @@
+cloning=📥 [git] Clonando repositorio '{repo}' en '{dest}'
+pulling=🔄 [git] Ejecutando git pull en '{dest}'
+checkout=📦 [git] Cambiando a rama '{branch}' en '{dest}'
+fetching=📄 [git] Extrayendo archivo '{file}' desde '{repo}' rama '{branch}'
+unsupported=❌ [git] Acción '{action}' no soportada
+missing_deps=❌ [git] Dependencias faltantes: {cmds}
+deps_ok=✅ [git] Todas las dependencias están disponibles
diff --git a/core/modules/groups.sh b/core/modules/groups.sh
new file mode 100644
index 0000000..e446b12
--- /dev/null
+++ b/core/modules/groups.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# Module: groups
+# Description: Gestiona grupos del sistema (crear, modificar, eliminar)
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: getent, groupadd, groupmod, groupdel, sudo
+
+groups_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local groupname="${args[groupname]}"
+ local gid="${args[gid]:-}"
+ local state="${args[state]:-create}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # 🛡️ Validación
+ if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then
+ echo "${tr[priv_required]:-❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'.}"
+ return 1
+ fi
+
+ if [[ -z "$groupname" ]]; then
+ echo "${tr[missing_groupname]:-❌ [groups] Falta el parámetro obligatorio 'groupname'}"
+ return 1
+ fi
+
+ case "$state" in
+ create)
+ echo "${tr[enter_create]:-🔧 [groups] Entrando en create}"
+ if getent group "$groupname" &>/dev/null; then
+ echo "$(render_msg "${tr[exists]}" "groupname=$groupname")"
+ return 0
+ fi
+ local cmd="$prefix groupadd \"$groupname\""
+ [[ -n "$gid" ]] && cmd="$cmd -g \"$gid\""
+ eval "$cmd" && echo "$(render_msg "${tr[created]}" "groupname=$groupname")"
+ ;;
+ modify)
+ echo "${tr[enter_modify]:-🔧 [groups] Entrando en modify}"
+ if ! getent group "$groupname" &>/dev/null; then
+ echo "$(render_msg "${tr[not_exists]}" "groupname=$groupname")"
+ return 1
+ fi
+ [[ -z "$gid" ]] && echo "${tr[nothing_to_modify]:-⚠️ [groups] Nada que modificar: falta 'gid'}" && return 0
+ eval "$prefix groupmod -g \"$gid\" \"$groupname\"" && echo "$(render_msg "${tr[modified]}" "groupname=$groupname")"
+ ;;
+ absent)
+ echo "${tr[enter_absent]:-🔧 [groups] Entrando en absent}"
+ if ! getent group "$groupname" &>/dev/null; then
+ echo "$(render_msg "${tr[already_deleted]}" "groupname=$groupname")"
+ return 0
+ fi
+ eval "$prefix groupdel \"$groupname\"" && echo "$(render_msg "${tr[deleted]}" "groupname=$groupname")"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_state]}" "state=$state")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_groups() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in getent sudo; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [groups] Todas las dependencias están presentes}"
+ return 0
+}
diff --git a/core/modules/groups.tr.en b/core/modules/groups.tr.en
new file mode 100644
index 0000000..6df77e1
--- /dev/null
+++ b/core/modules/groups.tr.en
@@ -0,0 +1,15 @@
+priv_required=❌ [groups] Privileges required to manage groups. Use 'become: true'.
+missing_groupname=❌ [groups] Missing required parameter 'groupname'
+enter_create=🔧 [groups] Entering create
+enter_modify=🔧 [groups] Entering modify
+enter_absent=🔧 [groups] Entering absent
+exists=✅ [groups] Group '{groupname}' already exists
+not_exists=⚠️ [groups] Group '{groupname}' does not exist
+nothing_to_modify=⚠️ [groups] Nothing to modify: missing 'gid'
+created=✅ [groups] Group '{groupname}' created
+modified=✅ [groups] Group '{groupname}' modified
+deleted=✅ [groups] Group '{groupname}' deleted
+already_deleted=✅ [groups] Group '{groupname}' already deleted
+unsupported_state=❌ [groups] Unsupported state '{state}'. Use create, modify or absent.
+missing_deps=❌ [groups] Missing dependencies: {cmds}
+deps_ok=✅ [groups] All dependencies are present
diff --git a/core/modules/groups.tr.es b/core/modules/groups.tr.es
new file mode 100644
index 0000000..70e1492
--- /dev/null
+++ b/core/modules/groups.tr.es
@@ -0,0 +1,15 @@
+priv_required=❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'.
+missing_groupname=❌ [groups] Falta el parámetro obligatorio 'groupname'
+enter_create=🔧 [groups] Entrando en create
+enter_modify=🔧 [groups] Entrando en modify
+enter_absent=🔧 [groups] Entrando en absent
+exists=✅ [groups] Grupo '{groupname}' ya existe
+not_exists=⚠️ [groups] Grupo '{groupname}' no existe
+nothing_to_modify=⚠️ [groups] Nada que modificar: falta 'gid'
+created=✅ [groups] Grupo '{groupname}' creado
+modified=✅ [groups] Grupo '{groupname}' modificado
+deleted=✅ [groups] Grupo '{groupname}' eliminado
+already_deleted=✅ [groups] Grupo '{groupname}' ya eliminado
+unsupported_state=❌ [groups] Estado '{state}' no soportado. Usa create, modify o absent.
+missing_deps=❌ [groups] Dependencias faltantes: {cmds}
+deps_ok=✅ [groups] Todas las dependencias están presentes
diff --git a/core/modules/lineinfile.sh b/core/modules/lineinfile.sh
new file mode 100644
index 0000000..aded42c
--- /dev/null
+++ b/core/modules/lineinfile.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env bash
+# Module: lineinfile
+# Description: Asegura la presencia o reemplazo de una línea en un archivo
+# Author: Luis GuLo
+# Version: 0.2.0
+# Dependencies: grep, sed, tee, awk
+
+lineinfile_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local path="${args[path]}"
+ local line="${args[line]}"
+ local regexp="${args[regexp]}"
+ local insert_after="${args[insert_after]}"
+ local create="${args[create]:-true}"
+ local backup="${args[backup]:-true}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if [[ ! -f "$path" ]]; then
+ if [[ "$create" == "true" ]]; then
+ echo "$(render_msg "${tr[creating]}" "path=$path")"
+ touch "$path"
+ else
+ echo "$(render_msg "${tr[missing_file]}" "path=$path")"
+ return 1
+ fi
+ fi
+
+ if [[ "$backup" == "true" ]]; then
+ cp "$path" "$path.bak"
+ echo "$(render_msg "${tr[backup]}" "path=$path")"
+ fi
+
+ if [[ -n "$regexp" && $(grep -Eq "$regexp" "$path" && echo "yes") == "yes" ]]; then
+ echo "$(render_msg "${tr[replacing]}" "regexp=$regexp")"
+ $prefix sed -i "s|$regexp|$line|" "$path"
+ return 0
+ fi
+
+ if [[ -n "$insert_after" && $(grep -q "$insert_after" "$path" && echo "yes") == "yes" ]]; then
+ echo "$(render_msg "${tr[inserting]}" "after=$insert_after")"
+ $prefix sed -i "/$insert_after/a $line" "$path"
+ return 0
+ fi
+
+ if grep -Fxq "$line" "$path"; then
+ echo "$(render_msg "${tr[exists]}" "line=$line")"
+ return 0
+ fi
+
+ echo "${tr[appending]:-➕ [lineinfile] Añadiendo línea al final}"
+ echo "$line" | $prefix tee -a "$path" > /dev/null
+}
+
+check_dependencies_lineinfile() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in grep sed tee awk; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [lineinfile] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/lineinfile.tr.en b/core/modules/lineinfile.tr.en
new file mode 100644
index 0000000..15054eb
--- /dev/null
+++ b/core/modules/lineinfile.tr.en
@@ -0,0 +1,9 @@
+creating=📄 [lineinfile] Creating file: {path}
+missing_file=❌ [lineinfile] File does not exist and create=false
+backup=📦 Backup created: {path}.bak
+replacing=🔁 [lineinfile] Replacing line matching: {regexp}
+inserting=➕ [lineinfile] Inserting after: {after}
+exists=✅ [lineinfile] Line already present: "{line}"
+appending=➕ [lineinfile] Appending line at the end
+missing_deps=❌ [lineinfile] Missing dependencies: {cmds}
+deps_ok=✅ [lineinfile] All dependencies are available
diff --git a/core/modules/lineinfile.tr.es b/core/modules/lineinfile.tr.es
new file mode 100644
index 0000000..c28d6ca
--- /dev/null
+++ b/core/modules/lineinfile.tr.es
@@ -0,0 +1,9 @@
+creating=📄 [lineinfile] Creando archivo: {path}
+missing_file=❌ [lineinfile] El archivo no existe y create=false
+backup=📦 Copia de seguridad creada: {path}.bak
+replacing=🔁 [lineinfile] Reemplazando línea que coincide con: {regexp}
+inserting=➕ [lineinfile] Insertando después de: {after}
+exists=✅ [lineinfile] Línea ya presente: "{line}"
+appending=➕ [lineinfile] Añadiendo línea al final
+missing_deps=❌ [lineinfile] Dependencias faltantes: {cmds}
+deps_ok=✅ [lineinfile] Todas las dependencias están disponibles
diff --git a/core/modules/lookup.sh b/core/modules/lookup.sh
new file mode 100644
index 0000000..d3c93bb
--- /dev/null
+++ b/core/modules/lookup.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# Module: lookup
+# Description: Recupera secretos cifrados del vault local
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: gpg
+
+lookup_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local vault_key="${args[key]}"
+ local vault_dir="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/core/vault"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if [[ -f "$vault_dir/$vault_key.gpg" ]]; then
+ gpg --quiet --batch --yes --passphrase-file "$HOME/.shflow.key" -d "$vault_dir/$vault_key.gpg" 2>/dev/null || \
+ gpg --quiet --batch --yes -d "$vault_dir/$vault_key.gpg"
+ else
+ echo "$(render_msg "${tr[not_found]}" "key=$vault_key" "path=$vault_dir")"
+ return 1
+ fi
+}
+
+check_dependencies_lookup() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v gpg &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [lookup] gpg no disponible.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [lookup] gpg disponible.}"
+ return 0
+}
diff --git a/core/modules/lookup.tr.en b/core/modules/lookup.tr.en
new file mode 100644
index 0000000..c20d4b0
--- /dev/null
+++ b/core/modules/lookup.tr.en
@@ -0,0 +1,3 @@
+not_found=❌ [lookup] Secret '{key}' not found in {path}
+missing_deps=❌ [lookup] gpg is not available.
+deps_ok=✅ [lookup] gpg is available.
diff --git a/core/modules/lookup.tr.es b/core/modules/lookup.tr.es
new file mode 100644
index 0000000..e1a89b7
--- /dev/null
+++ b/core/modules/lookup.tr.es
@@ -0,0 +1,3 @@
+not_found=❌ [lookup] Secreto '{key}' no encontrado en {path}
+missing_deps=❌ [lookup] gpg no disponible.
+deps_ok=✅ [lookup] gpg disponible.
diff --git a/core/modules/loop.sh b/core/modules/loop.sh
new file mode 100644
index 0000000..3f7cb0d
--- /dev/null
+++ b/core/modules/loop.sh
@@ -0,0 +1,130 @@
+#!/usr/bin/env bash
+# Module: loop
+# Description: Ejecuta un módulo sobre una lista o matriz de valores
+# Author: Luis GuLo
+# Version: 0.3.0
+# Dependencies: echo, tee
+
+loop_task() {
+ local host="$1"; shift
+ declare -A args
+ local items_raw="" secondary_raw="" target_module=""
+ local fail_fast="true"
+ declare -A module_args
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # Parsear argumentos
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ case "$key" in
+ items) items_raw="$value" ;;
+ secondary) secondary_raw="$value" ;;
+ module) target_module="$value" ;;
+ fail_fast) fail_fast="$value" ;;
+ *) module_args["$key"]="$value" ;;
+ esac
+ done
+
+ if [[ -z "$items_raw" || -z "$target_module" ]]; then
+ echo "${tr[missing_args]:-❌ [loop] Faltan argumentos obligatorios: items=... module=...}"
+ return 1
+ fi
+
+ IFS=',' read -r -a items <<< "$items_raw"
+ IFS=',' read -r -a secondary <<< "$secondary_raw"
+
+ for item in "${items[@]}"; do
+ if [[ "$item" == *:* ]]; then
+ item_key="${item%%:*}"
+ item_value="${item#*:}"
+ else
+ item_key="$item"
+ item_value=""
+ fi
+
+ if [[ -n "$secondary_raw" ]]; then
+ for sec in "${secondary[@]}"; do
+ run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "$sec" module_args || {
+ echo "$(render_msg "${tr[fail_secondary]}" "item=$item" "secondary=$sec")"
+ [[ "$fail_fast" == "true" ]] && return 1
+ }
+ done
+ else
+ run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "" module_args || {
+ echo "$(render_msg "${tr[fail_item]}" "item=$item")"
+ [[ "$fail_fast" == "true" ]] && return 1
+ }
+ fi
+ done
+}
+
+run_module() {
+ local host="$1"
+ local module="$2"
+ local item="$3"
+ local item_key="$4"
+ local item_value="$5"
+ local secondary_item="$6"
+ declare -n args_ref="$7"
+
+ local call_args=()
+ for key in "${!args_ref[@]}"; do
+ value="${args_ref[$key]}"
+ value="${value//\{\{item\}\}/$item}"
+ value="${value//\{\{item_key\}\}/$item_key}"
+ value="${value//\{\{item_value\}\}/$item_value}"
+ value="${value//\{\{secondary_item\}\}/$secondary_item}"
+ call_args+=("$key=$value")
+ done
+
+ echo "🔁 [loop] → $module con item='$item' secondary='$secondary_item'"
+
+ local PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+ local MODULE_PATH=""
+ local SEARCH_PATHS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules")
+ for search_dir in "${SEARCH_PATHS[@]}"; do
+ while IFS= read -r -d '' candidate; do
+ [[ "$(basename "$candidate")" == "${module}.sh" ]] && MODULE_PATH="$candidate" && break 2
+ done < <(find "$search_dir" -type f -name "${module}.sh" -print0)
+ done
+
+ if [[ -z "$MODULE_PATH" ]]; then
+ echo "$(render_msg "${tr[module_not_found]}" "module=$module")"
+ return 1
+ fi
+
+ source "$MODULE_PATH"
+ ! declare -f "${module}_task" > /dev/null && echo "$(render_msg "${tr[task_not_found]}" "module=$module")" && return 1
+
+ "${module}_task" "$host" "${call_args[@]}"
+}
+
+check_dependencies_loop() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in echo tee; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [loop] Dependencias disponibles.}"
+ return 0
+}
diff --git a/core/modules/loop.tr.en b/core/modules/loop.tr.en
new file mode 100644
index 0000000..d0de2dd
--- /dev/null
+++ b/core/modules/loop.tr.en
@@ -0,0 +1,7 @@
+missing_args=❌ [loop] Missing required arguments: items=... module=...
+fail_item=⚠️ [loop] Iteration failed with '{item}'
+fail_secondary=⚠️ [loop] Iteration failed with '{item}' and '{secondary}'
+module_not_found=❌ [loop] Module '{module}' not found
+task_not_found=❌ [loop] Function '{module}_task' not found
+missing_deps=❌ [loop] Missing dependencies: {cmds}
+deps_ok=✅ [loop] Dependencies available.
diff --git a/core/modules/loop.tr.es b/core/modules/loop.tr.es
new file mode 100644
index 0000000..aaf85ec
--- /dev/null
+++ b/core/modules/loop.tr.es
@@ -0,0 +1,7 @@
+missing_args=❌ [loop] Faltan argumentos obligatorios: items=... module=...
+fail_item=⚠️ [loop] Falló la iteración con '{item}'
+fail_secondary=⚠️ [loop] Falló la iteración con '{item}' y '{secondary}'
+module_not_found=❌ [loop] Módulo '{module}' no encontrado
+task_not_found=❌ [loop] Función '{module}_task' no encontrada
+missing_deps=❌ [loop] Dependencias faltantes: {cmds}
+deps_ok=✅ [loop] Dependencias disponibles.
diff --git a/core/modules/openssl.sh b/core/modules/openssl.sh
new file mode 100644
index 0000000..d872dca
--- /dev/null
+++ b/core/modules/openssl.sh
@@ -0,0 +1,141 @@
+#!/bin/bash
+# Module: openssl
+# Description: Gestiona certificados y claves con OpenSSL (convertir, inspeccionar, instalar como CA)
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: openssl, sudo, bash
+
+openssl_task() {
+ local host="$1"; shift
+ check_dependencies_openssl || return 1
+
+ local state="" src="" dest="" format="" password="" alias="" trust_path="" become="false"
+ for arg in "$@"; do
+ case "$arg" in
+ state=*) state="${arg#state=}" ;;
+ src=*) src="${arg#src=}" ;;
+ dest=*) dest="${arg#dest=}" ;;
+ format=*) format="${arg#format=}" ;;
+ password=*) password="${arg#password=}" ;;
+ alias=*) alias="${arg#alias=}" ;;
+ trust_path=*) trust_path="${arg#trust_path=}" ;;
+ become=*) become="${arg#become=}" ;;
+ esac
+ done
+
+ local sudo_cmd=""
+ [[ "$become" == "true" ]] && sudo_cmd="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ case "$state" in
+ convert)
+ if [[ -z "$src" || -z "$dest" || -z "$format" ]]; then
+ echo "${tr[missing_convert]:-❌ [openssl] Faltan argumentos para conversión: src, dest, format}"
+ return 1
+ fi
+ if [[ ! -f "$src" ]]; then
+ echo "$(render_msg "${tr[src_not_found]}" "src=$src")"
+ return 1
+ fi
+ echo "$(render_msg "${tr[converting]}" "src=$src" "format=$format")"
+
+ case "$format" in
+ pem)
+ $sudo_cmd openssl pkcs12 -in "$src" -out "$dest" -nodes -password pass:"$password" && \
+ echo "$(render_msg "${tr[converted]}" "dest=$dest")"
+ ;;
+ pfx)
+ $sudo_cmd openssl pkcs12 -export -out "$dest" -inkey "$src" -in "$src" -password pass:"$password" && \
+ echo "$(render_msg "${tr[converted]}" "dest=$dest")"
+ ;;
+ key)
+ $sudo_cmd openssl pkey -in "$src" -out "$dest" && \
+ echo "$(render_msg "${tr[key_extracted]}" "dest=$dest")"
+ ;;
+ cer)
+ $sudo_cmd openssl x509 -in "$src" -out "$dest" -outform DER && \
+ echo "$(render_msg "${tr[cer_converted]}" "dest=$dest")"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"
+ return 1
+ ;;
+ esac
+ ;;
+
+ inspect)
+ if [[ -z "$src" || ! -f "$src" ]]; then
+ echo "$(render_msg "${tr[missing_inspect]}" "src=$src")"
+ return 1
+ fi
+ echo "$(render_msg "${tr[inspecting]}" "src=$src")"
+ $sudo_cmd openssl x509 -in "$src" -noout -text | grep -E 'Subject:|Issuer:|Not Before:|Not After :|Fingerprint' || echo "${tr[inspect_fail]:-⚠️ [openssl] No se pudo extraer información}"
+ ;;
+
+ trust)
+ if [[ -z "$src" || -z "$alias" || -z "$trust_path" ]]; then
+ echo "${tr[missing_trust]:-❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path}"
+ return 1
+ fi
+ if [[ ! -f "$src" ]]; then
+ echo "$(render_msg "${tr[src_not_found]}" "src=$src")"
+ return 1
+ fi
+ echo "$(render_msg "${tr[trusting]}" "alias=$alias")"
+ $sudo_cmd cp "$src" "$trust_path/$alias.crt" && \
+ $sudo_cmd update-ca-certificates && \
+ echo "${tr[trusted]:-✅ [openssl] Certificado instalado y CA actualizada}"
+ ;;
+
+ untrust)
+ if [[ -z "$alias" || -z "$trust_path" ]]; then
+ echo "${tr[missing_untrust]:-❌ [openssl] Faltan argumentos para eliminación: alias, trust_path}"
+ return 1
+ fi
+ local cert_path="$trust_path/$alias.crt"
+ if [[ ! -f "$cert_path" ]]; then
+ echo "$(render_msg "${tr[untrust_not_found]}" "alias=$alias" "trust_path=$trust_path")"
+ return 0
+ fi
+ echo "$(render_msg "${tr[untrusting]}" "alias=$alias")"
+ $sudo_cmd rm -f "$cert_path" && \
+ $sudo_cmd update-ca-certificates && \
+ echo "${tr[untrusted]:-✅ [openssl] Certificado eliminado y CA actualizada}"
+ ;;
+
+ *)
+ echo "$(render_msg "${tr[unknown_state]}" "state=$state")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_openssl() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in openssl sudo; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [openssl] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/openssl.tr.en b/core/modules/openssl.tr.en
new file mode 100644
index 0000000..a0caffb
--- /dev/null
+++ b/core/modules/openssl.tr.en
@@ -0,0 +1,20 @@
+missing_convert=❌ [openssl] Missing arguments for conversion: src, dest, format
+missing_inspect=❌ [openssl] File not found for inspection: {src}
+missing_trust=❌ [openssl] Missing arguments for CA installation: src, alias, trust_path
+missing_untrust=❌ [openssl] Missing arguments for removal: alias, trust_path
+src_not_found=❌ [openssl] Input file not found: {src}
+converting=🔐 [openssl] Converting {src} to format {format}...
+converted=✅ [openssl] Conversion completed: {dest}
+key_extracted=✅ [openssl] Key extracted: {dest}
+cer_converted=✅ [openssl] Certificate converted to .cer: {dest}
+unsupported_format=❌ [openssl] Unsupported destination format: {format}
+inspecting=🔍 [openssl] Inspecting certificate: {src}
+inspect_fail=⚠️ [openssl] Failed to extract certificate information
+trusting=🏛️ [openssl] Installing certificate '{alias}' as trusted CA...
+trusted=✅ [openssl] Certificate installed and CA updated
+untrust_not_found=⚠️ [openssl] Certificate '{alias}' not found in {trust_path}
+untrusting=🧹 [openssl] Removing certificate '{alias}' from trusted CAs...
+untrusted=✅ [openssl] Certificate removed and CA updated
+unknown_state=❌ [openssl] Unknown state: '{state}'. Use 'convert', 'inspect', 'trust' or 'untrust'
+missing_deps=❌ [openssl] Missing dependencies: {cmds}
+deps_ok=✅ [openssl] All dependencies are available
diff --git a/core/modules/openssl.tr.es b/core/modules/openssl.tr.es
new file mode 100644
index 0000000..0b86cc0
--- /dev/null
+++ b/core/modules/openssl.tr.es
@@ -0,0 +1,20 @@
+missing_convert=❌ [openssl] Faltan argumentos para conversión: src, dest, format
+missing_inspect=❌ [openssl] Archivo no encontrado para inspección: {src}
+missing_trust=❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path
+missing_untrust=❌ [openssl] Faltan argumentos para eliminación: alias, trust_path
+src_not_found=❌ [openssl] Archivo no encontrado: {src}
+converting=🔐 [openssl] Convirtiendo {src} a formato {format}...
+converted=✅ [openssl] Conversión completada: {dest}
+key_extracted=✅ [openssl] Clave extraída: {dest}
+cer_converted=✅ [openssl] Certificado convertido a .cer: {dest}
+unsupported_format=❌ [openssl] Formato de destino no soportado: {format}
+inspecting=🔍 [openssl] Inspeccionando certificado: {src}
+inspect_fail=⚠️ [openssl] No se pudo extraer información
+trusting=🏛️ [openssl] Instalando certificado '{alias}' como CA...
+trusted=✅ [openssl] Certificado instalado y CA actualizada
+untrust_not_found=⚠️ [openssl] Certificado '{alias}' no encontrado en {trust_path}
+untrusting=🧹 [openssl] Eliminando certificado '{alias}' de CA confiables...
+untrusted=✅ [openssl] Certificado eliminado y CA actualizada
+unknown_state=❌ [openssl] Estado desconocido: '{state}'. Usa 'convert', 'inspect', 'trust' o 'untrust'
+missing_deps=❌ [openssl] Dependencias faltantes: {cmds}
+deps_ok=✅ [openssl] Todas las dependencias están disponibles
diff --git a/core/modules/package.sh b/core/modules/package.sh
new file mode 100644
index 0000000..7e41161
--- /dev/null
+++ b/core/modules/package.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+# Module: package
+# Description: Instala, actualiza o elimina paquetes .deb/.rpm y permite actualizar el sistema
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 2.2.0
+# Dependencies: ssh
+
+package_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local name="${args[name]:-}"
+ local state="${args[state]:-present}"
+ local become="${args[become]:-false}"
+ local update_type="${args[update_type]:-full}" # full | security
+
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/package.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile"
+ fi
+
+ echo "$(render_msg "${tr[start]}" "state=$state" "name=${name:-<sistema>}")"
+
+ local pkg_mgr
+ pkg_mgr=$(ssh "$host" "command -v apt-get || command -v apt || command -v dnf || command -v yum")
+
+ if [ -z "$pkg_mgr" ]; then
+ echo "${tr[no_pkg_mgr]:-❌ [package] No se detectó gestor de paquetes compatible en el host.}"
+ return 1
+ fi
+
+ case "$pkg_mgr" in
+ *apt*)
+ if [ "$state" = "system-update" ]; then
+ system_update_apt "$host" "$prefix"
+ else
+ package_apt "$host" "$name" "$state" "$prefix"
+ fi
+ ;;
+ *yum*|*dnf*)
+ if [ "$state" = "system-update" ]; then
+ system_update_rpm "$host" "$prefix" "$update_type"
+ else
+ package_rpm "$host" "$name" "$state" "$prefix"
+ fi
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_mgr]}" "mgr=$pkg_mgr")"
+ return 1
+ ;;
+ esac
+}
+
+package_apt() {
+ local host="$1"
+ local name="$2"
+ local state="$3"
+ local prefix="$4"
+
+ local check_cmd="dpkg -s '$name' &> /dev/null"
+ local install_cmd="$prefix apt-get update && $prefix apt-get install -y '$name'"
+ local remove_cmd="$prefix apt-get remove -y '$name'"
+ local upgrade_cmd="$prefix apt-get update && $prefix apt-get install --only-upgrade -y '$name'"
+
+ case "$state" in
+ present) ssh "$host" "$check_cmd || $install_cmd" ;;
+ absent) ssh "$host" "$check_cmd && $remove_cmd" ;;
+ latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;;
+ *) echo "$(render_msg "${tr[unsupported_state_apt]}" "state=$state")"; return 1 ;;
+ esac
+}
+
+package_rpm() {
+ local host="$1"
+ local name="$2"
+ local state="$3"
+ local prefix="$4"
+
+ local check_cmd="rpm -q '$name' &> /dev/null"
+ local install_cmd="$prefix yum install -y '$name' || $prefix dnf install -y '$name'"
+ local remove_cmd="$prefix yum remove -y '$name' || $prefix dnf remove -y '$name'"
+ local upgrade_cmd="$prefix yum update -y '$name' || $prefix dnf upgrade -y '$name'"
+
+ case "$state" in
+ present) ssh "$host" "$check_cmd || $install_cmd" ;;
+ absent) ssh "$host" "$check_cmd && $remove_cmd" ;;
+ latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;;
+ *) echo "$(render_msg "${tr[unsupported_state_rpm]}" "state=$state")"; return 1 ;;
+ esac
+}
+
+system_update_apt() {
+ local host="$1"
+ local prefix="$2"
+ echo "${tr[update_apt]:-🔄 [package] Actualización completa del sistema (.deb)}"
+ ssh "$host" "$prefix apt-get update && $prefix apt-get upgrade -y"
+}
+
+system_update_rpm() {
+ local host="$1"
+ local prefix="$2"
+ local update_type="$3"
+
+ if [ "$update_type" = "security" ]; then
+ echo "${tr[update_rpm_security]:-🔐 [package] Actualización de seguridad (.rpm)}"
+ ssh "$host" "$prefix dnf update --security -y || $prefix yum update --security -y"
+ else
+ echo "${tr[update_rpm_full]:-🔄 [package] Actualización completa del sistema (.rpm)}"
+ ssh "$host" "$prefix dnf upgrade --refresh -y || $prefix yum update -y"
+ fi
+}
+
+check_dependencies_package() {
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [package] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [package] ssh disponible.}"
+ return 0
+}
diff --git a/core/modules/package.tr.en b/core/modules/package.tr.en
new file mode 100644
index 0000000..baae9c5
--- /dev/null
+++ b/core/modules/package.tr.en
@@ -0,0 +1,10 @@
+start=📦 [package] State: {state} | Package: {name} | Manager: detecting...
+no_pkg_mgr=❌ [package] No compatible package manager detected on host.
+unsupported_mgr=❌ [package] Manager '{mgr}' not supported.
+unsupported_state_apt=❌ [package] State '{state}' not supported for APT.
+unsupported_state_rpm=❌ [package] State '{state}' not supported for RPM.
+update_apt=🔄 [package] Full system update (.deb)
+update_rpm_security=🔐 [package] Security update (.rpm)
+update_rpm_full=🔄 [package] Full system update (.rpm)
+missing_deps=❌ [package] ssh is not available.
+deps_ok=✅ [package] ssh is available.
diff --git a/core/modules/package.tr.es b/core/modules/package.tr.es
new file mode 100644
index 0000000..ed7c4fa
--- /dev/null
+++ b/core/modules/package.tr.es
@@ -0,0 +1,10 @@
+start=📦 [package] Estado: {state} | Paquete: {name} | Gestor: detectando...
+no_pkg_mgr=❌ [package] No se detectó gestor de paquetes compatible en el host.
+unsupported_mgr=❌ [package] Gestor '{mgr}' no soportado.
+unsupported_state_apt=❌ [package] Estado '{state}' no soportado para APT.
+unsupported_state_rpm=❌ [package] Estado '{state}' no soportado para RPM.
+update_apt=🔄 [package] Actualización completa del sistema (.deb)
+update_rpm_security=🔐 [package] Actualización de seguridad (.rpm)
+update_rpm_full=🔄 [package] Actualización completa del sistema (.rpm)
+missing_deps=❌ [package] ssh no está disponible.
+deps_ok=✅ [package] ssh disponible.
diff --git a/core/modules/ping.sh b/core/modules/ping.sh
new file mode 100644
index 0000000..79b72c6
--- /dev/null
+++ b/core/modules/ping.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Module: ping
+# Description: Verifica conectividad desde el host remoto hacia un destino específico
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2
+# Dependencies: ping, ssh
+
+ping_task() {
+ local host="$1"; shift
+ declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local count="${args[count]:-2}"
+ local timeout="${args[timeout]:-3}"
+ local target="${args[target]:-127.0.0.1}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile"
+ else
+ echo "⚠️ [ping] Archivo de traducción no encontrado: $trfile"
+ fi
+
+ echo "$(render_msg "${tr[start]}" "host=$host" "target=$target")"
+
+ if ssh "$host" "$prefix ping -c $count -W $timeout $target &>/dev/null"; then
+ echo " $(render_msg "${tr[success]}" "host=$host" "target=$target")"
+ return 0
+ else
+ echo " $(render_msg "${tr[fail]}" "host=$host" "target=$target")"
+ return 1
+ fi
+}
+
+check_dependencies_ping() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null || ! command -v ping &> /dev/null; then
+ echo " ${tr[missing_deps]:-❌ [ping] ssh o ping no están disponibles.}"
+ return 1
+ fi
+ echo " ${tr[deps_ok]:-✅ [ping] ssh y ping disponibles.}"
+ return 0
+}
diff --git a/core/modules/ping.tr.en b/core/modules/ping.tr.en
new file mode 100644
index 0000000..1faf4fe
--- /dev/null
+++ b/core/modules/ping.tr.en
@@ -0,0 +1,5 @@
+start=📡 [ping] Testing connectivity from {host} to {target}...
+success=✅ [ping] {host} can reach {target}
+fail=❌ [ping] {host} cannot reach {target}
+missing_deps=❌ [ping] ssh or ping are not available.
+deps_ok=✅ [ping] ssh and ping are available.
diff --git a/core/modules/ping.tr.es b/core/modules/ping.tr.es
new file mode 100644
index 0000000..37c3805
--- /dev/null
+++ b/core/modules/ping.tr.es
@@ -0,0 +1,5 @@
+start=📡 [ping] Probando conectividad desde {host} hacia {target}...
+success=✅ [ping] {host} puede alcanzar {target}
+fail=❌ [ping] {host} no puede alcanzar {target}
+missing_deps=❌ [ping] ssh o ping no están disponibles.
+deps_ok=✅ [ping] ssh y ping disponibles.
diff --git a/core/modules/replace.sh b/core/modules/replace.sh
new file mode 100644
index 0000000..fb961ea
--- /dev/null
+++ b/core/modules/replace.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+# Module: replace
+# Description: Reemplaza texto en archivos usando expresiones regulares
+# Author: Luis GuLo
+# Version: 0.2.0
+# Dependencies: sed, cp, tee
+
+replace_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local path="${args[path]}"
+ local regexp="${args[regexp]}"
+ local replace="${args[replace]}"
+ local backup="${args[backup]:-true}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if [[ ! -f "$path" ]]; then
+ echo "$(render_msg "${tr[missing_file]}" "path=$path")"
+ return 1
+ fi
+
+ if [[ "$backup" == "true" ]]; then
+ cp "$path" "$path.bak"
+ echo "$(render_msg "${tr[backup_created]}" "path=$path")"
+ fi
+
+ $prefix sed -i "s|$regexp|$replace|g" "$path"
+ echo "$(render_msg "${tr[replaced]}" "path=$path")"
+}
+
+check_dependencies_replace() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in sed cp tee; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [replace] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/replace.tr.en b/core/modules/replace.tr.en
new file mode 100644
index 0000000..f4ec4ae
--- /dev/null
+++ b/core/modules/replace.tr.en
@@ -0,0 +1,5 @@
+missing_file=❌ [replace] File not found: {path}
+backup_created=📦 Backup created: {path}.bak
+replaced=✅ [replace] Replacement applied to: {path}
+missing_deps=❌ [replace] Missing dependencies: {cmds}
+deps_ok=✅ [replace] All dependencies are available
diff --git a/core/modules/replace.tr.es b/core/modules/replace.tr.es
new file mode 100644
index 0000000..f6e647e
--- /dev/null
+++ b/core/modules/replace.tr.es
@@ -0,0 +1,5 @@
+missing_file=❌ [replace] El archivo no existe: {path}
+backup_created=📦 Copia de seguridad creada: {path}.bak
+replaced=✅ [replace] Reemplazo aplicado en: {path}
+missing_deps=❌ [replace] Dependencias faltantes: {cmds}
+deps_ok=✅ [replace] Todas las dependencias están disponibles
diff --git a/core/modules/run.sh b/core/modules/run.sh
new file mode 100644
index 0000000..4ec4e9b
--- /dev/null
+++ b/core/modules/run.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+# Module: run
+# Description: Ejecuta comandos remotos vía SSH, con soporte para vault y sudo
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 2.0.0
+# Dependencies: ssh, core/utils/vault_utils.sh
+
+# Detectar raíz del proyecto si no está definida
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+
+# Cargar utilidades
+source "$PROJECT_ROOT/core/utils/vault_utils.sh"
+
+run_task() {
+ local host="$1"; shift
+ declare -A args
+
+ while [[ "$#" -gt 0 ]]; do
+ case "$1" in
+ *=*)
+ key="${1%%=*}"
+ value="${1#*=}"
+ args["$key"]="$value"
+ ;;
+ esac
+ shift
+ done
+
+ local command="${args[command]}"
+ local become="${args[become]:-}"
+ local vault_key="${args[vault_key]:-}"
+
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$val"; done < "$trfile"
+ fi
+
+ # 🧠 Comandos que no deben ejecutarse con sudo
+ local safe_cmds=("echo" "true" "false" "command" "which" "exit" "test")
+ local first_cmd="${command%% *}"
+ for safe in "${safe_cmds[@]}"; do
+ if [[ "$first_cmd" == "$safe" ]]; then
+ prefix=""
+ break
+ fi
+ done
+
+ # 🔁 Interpolación de variables ShFlow
+ for var in $(compgen -A variable | grep '^shflow_vars_'); do
+ key="${var#shflow_vars_}"
+ value="${!var}"
+ command="${command//\{\{ $key \}\}/$value}"
+ done
+
+ echo "$(render_msg "${tr[start]}" "host=$host" "command=$command" "prefix=$prefix")"
+
+ if [ -n "$vault_key" ]; then
+ local secret
+ secret=$(get_secret "$vault_key") || {
+ echo "$(render_msg "${tr[vault_fail]}" "vault_key=$vault_key")"
+ return 1
+ }
+ ssh "$host" "$prefix TOKEN='$secret' $command"
+ else
+ ssh "$host" "$prefix $command"
+ fi
+}
+
+check_dependencies_run() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_deps]:-❌ [run] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[deps_ok]:-✅ [run] ssh disponible.}"
+ return 0
+}
diff --git a/core/modules/run.tr.en b/core/modules/run.tr.en
new file mode 100644
index 0000000..55a3f3e
--- /dev/null
+++ b/core/modules/run.tr.en
@@ -0,0 +1,4 @@
+start=📡 [run] Executing on {host}: {prefix} {command}
+vault_fail=❌ [run] Failed to retrieve secret '{vault_key}'
+missing_deps=❌ [run] ssh is not available.
+deps_ok=✅ [run] ssh is available.
diff --git a/core/modules/run.tr.es b/core/modules/run.tr.es
new file mode 100644
index 0000000..20b7f2f
--- /dev/null
+++ b/core/modules/run.tr.es
@@ -0,0 +1,4 @@
+start=📡 [run] Ejecutando en {host}: {prefix} {command}
+vault_fail=❌ [run] No se pudo obtener el secreto '{vault_key}'
+missing_deps=❌ [run] ssh no está disponible.
+deps_ok=✅ [run] ssh disponible.
diff --git a/core/modules/service.sh b/core/modules/service.sh
new file mode 100644
index 0000000..bda0cd3
--- /dev/null
+++ b/core/modules/service.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Module: service
+# Description: Controla servicios del sistema remoto (start, stop, restart, enable, disable) con idempotencia
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: ssh, systemctl
+
+service_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local name="${args[name]}"
+ local state="${args[state]}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ case "$state" in
+ start|stop|restart|enable|disable)
+ echo "$(render_msg "${tr[executing]}" "state=$state" "name=$name" "host=$host")"
+ ssh "$host" "$prefix systemctl $state '$name'"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "state=$state")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_service() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if ! command -v ssh &> /dev/null; then
+ echo "${tr[missing_ssh]:-❌ [service] ssh no está disponible.}"
+ return 1
+ fi
+ echo "${tr[ssh_ok]:-✅ [service] ssh disponible.}"
+
+ if ! command -v systemctl &> /dev/null; then
+ echo "${tr[missing_systemctl]:-⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto.}"
+ else
+ echo "${tr[systemctl_ok]:-✅ [service] systemctl disponible localmente.}"
+ fi
+ return 0
+}
diff --git a/core/modules/service.tr.en b/core/modules/service.tr.en
new file mode 100644
index 0000000..87dc131
--- /dev/null
+++ b/core/modules/service.tr.en
@@ -0,0 +1,6 @@
+executing=🔧 [service] Executing '{state}' on service '{name}' at {host}
+unsupported=❌ [service] State '{state}' not supported. Use start, stop, restart, enable or disable.
+missing_ssh=❌ [service] ssh is not available.
+ssh_ok=✅ [service] ssh is available.
+missing_systemctl=⚠️ [service] systemctl is not available locally. Assuming it exists on the remote host.
+systemctl_ok=✅ [service] systemctl is available locally.
diff --git a/core/modules/service.tr.es b/core/modules/service.tr.es
new file mode 100644
index 0000000..26c4032
--- /dev/null
+++ b/core/modules/service.tr.es
@@ -0,0 +1,6 @@
+executing=🔧 [service] Ejecutando '{state}' sobre el servicio '{name}' en {host}
+unsupported=❌ [service] Estado '{state}' no soportado. Usa start, stop, restart, enable o disable.
+missing_ssh=❌ [service] ssh no está disponible.
+ssh_ok=✅ [service] ssh disponible.
+missing_systemctl=⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto.
+systemctl_ok=✅ [service] systemctl disponible localmente.
diff --git a/core/modules/smtp_send.sh b/core/modules/smtp_send.sh
new file mode 100644
index 0000000..6961ec4
--- /dev/null
+++ b/core/modules/smtp_send.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+# Module: smtp_send
+# Description: Envía un correo de prueba usando SMTP con netcat o openssl s_client
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 0.2.0
+# Dependencies: nc o openssl, base64
+
+smtp_send_task() {
+ declare -A args
+ for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local smtp_server="${args[smtp_server]}"
+ local smtp_port="${args[smtp_port]:-587}"
+ local smtp_user="${args[smtp_user]}"
+ local smtp_pass="${args[smtp_pass]}"
+ local from="${args[from]}"
+ local to="${args[to]}"
+ local subject="${args[subject]:-Prueba desde ShFlow}"
+ local body="${args[body]:-Este es un correo de prueba enviado desde el módulo smtp_send.}"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if [[ -z "$smtp_server" || -z "$smtp_user" || -z "$smtp_pass" || -z "$from" || -z "$to" ]]; then
+ echo "${tr[missing_args]:-❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to}"
+ return 1
+ fi
+
+ echo "$(render_msg "${tr[start]}" "to=$to" "server=$smtp_server" "port=$smtp_port")"
+
+ local auth_user auth_pass
+ auth_user=$(echo -n "$smtp_user" | base64)
+ auth_pass=$(echo -n "$smtp_pass" | base64)
+
+ local smtp_script
+ smtp_script=$(cat <<EOF
+EHLO localhost
+AUTH LOGIN
+$auth_user
+$auth_pass
+MAIL FROM:<$from>
+RCPT TO:<$to>
+DATA
+Subject: $subject
+From: $from
+To: $to
+
+$body
+.
+QUIT
+EOF
+)
+
+ if command -v nc &>/dev/null; then
+ echo "${tr[using_nc]:-🔧 Usando netcat para conexión SMTP...}"
+ echo "$smtp_script" | nc "$smtp_server" "$smtp_port"
+ elif command -v openssl &>/dev/null; then
+ echo "${tr[using_openssl]:-🔧 Usando openssl para conexión STARTTLS...}"
+ echo "$smtp_script" | openssl s_client -starttls smtp -crlf -connect "$smtp_server:$smtp_port" 2>/dev/null
+ else
+ echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema.}"
+ return 1
+ fi
+
+ echo "${tr[done]:-✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido.}"
+}
+
+check_dependencies_smtp_send() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ if command -v nc &>/dev/null || command -v openssl &>/dev/null; then
+ if ! command -v base64 &>/dev/null; then
+ echo "${tr[missing_base64]:-❌ [smtp_send] Falta base64 en el sistema.}"
+ return 1
+ fi
+ local tool=$(command -v nc &>/dev/null && echo "nc" || echo "openssl")
+ echo "$(render_msg "${tr[deps_ok]}" "tool=$tool")"
+ return 0
+ else
+ echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni nc ni openssl.}"
+ return 1
+ fi
+}
diff --git a/core/modules/smtp_send.tr.en b/core/modules/smtp_send.tr.en
new file mode 100644
index 0000000..f99ba99
--- /dev/null
+++ b/core/modules/smtp_send.tr.en
@@ -0,0 +1,8 @@
+missing_args=❌ [smtp_send] Missing required arguments: smtp_server, smtp_user, smtp_pass, from, to
+start=📡 [smtp_send] Preparing to send to {to} via {server}:{port}...
+using_nc=🔧 Using netcat for SMTP connection...
+using_openssl=🔧 Using openssl for STARTTLS connection...
+missing_tools=❌ [smtp_send] Neither netcat (nc) nor openssl found on system.
+missing_base64=❌ [smtp_send] base64 is missing on the system.
+deps_ok=✅ [smtp_send] Tools available: {tool}, base64
+done=✅ [smtp_send] Command executed. Check if the email was received.
diff --git a/core/modules/smtp_send.tr.es b/core/modules/smtp_send.tr.es
new file mode 100644
index 0000000..4fa0b99
--- /dev/null
+++ b/core/modules/smtp_send.tr.es
@@ -0,0 +1,8 @@
+missing_args=❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to
+start=📡 [smtp_send] Preparando envío a {to} vía {server}:{port}...
+using_nc=🔧 Usando netcat para conexión SMTP...
+using_openssl=🔧 Usando openssl para conexión STARTTLS...
+missing_tools=❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema.
+missing_base64=❌ [smtp_send] Falta base64 en el sistema.
+deps_ok=✅ [smtp_send] Herramientas disponibles: {tool}, base64
+done=✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido.
diff --git a/core/modules/template.sh b/core/modules/template.sh
new file mode 100644
index 0000000..887a47b
--- /dev/null
+++ b/core/modules/template.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+# Module: template
+# Description: Genera archivos a partir de plantillas con variables {{var}}, bucles, includes y delimitadores configurables
+# Author: Luis GuLo
+# Version: 0.4.0
+# Dependencies: bash, sed, tee, grep, cat
+
+# 🧭 Detección de raíz del proyecto
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+TEMPLATE_DIR="$PROJECT_ROOT/core/templates"
+
+template_task() {
+ local host="$1"; shift
+ declare -A args
+ local start_delim="{{" end_delim="}}" strict="false"
+ declare -A vars
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # Parsear argumentos
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ case "$key" in
+ src) src="$value" ;;
+ dest) dest="$value" ;;
+ become) become="$value" ;;
+ delimiters)
+ start_delim="${value%% *}"
+ end_delim="${value#* }"
+ ;;
+ strict) strict="$value" ;;
+ *) vars["$key"]="$value" ;;
+ esac
+ done
+
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ local template_path="$TEMPLATE_DIR/$src"
+ [[ ! -f "$template_path" ]] && echo "$(render_msg "${tr[missing_template]}" "path=$template_path")" && return 1
+
+ local rendered=""
+ local line loop_active="false" loop_key="" loop_buffer=()
+
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ${start_delim}[[:space:]]*include[[:space:]]*\"([^\"]+)\"[[:space:]]*${end_delim} ]]; then
+ include_file="${BASH_REMATCH[1]}"
+ include_path="$TEMPLATE_DIR/$include_file"
+ [[ -f "$include_path" ]] && rendered+=$(cat "$include_path")$'\n'
+ continue
+ fi
+
+ if [[ "$line" =~ ^#LOOP[[:space:]]+([a-zA-Z0-9_]+)$ ]]; then
+ loop_active="true"
+ loop_key="${BASH_REMATCH[1]}"
+ loop_buffer=()
+ continue
+ fi
+
+ if [[ "$line" == "#ENDLOOP" ]]; then
+ loop_active="false"
+ IFS=',' read -r -a items <<< "${vars[$loop_key]}"
+ for item in "${items[@]}"; do
+ for loop_line in "${loop_buffer[@]}"; do
+ rendered+=$(replace_vars "$loop_line" "$item" "$start_delim" "$end_delim")$'\n'
+ done
+ done
+ continue
+ fi
+
+ if [[ "$loop_active" == "true" ]]; then
+ loop_buffer+=("$line")
+ continue
+ fi
+
+ rendered+=$(replace_vars "$line" "" "$start_delim" "$end_delim")$'\n'
+ done < "$template_path"
+
+ if [[ "$strict" == "true" ]]; then
+ missing=$(echo "$rendered" | grep -o "${start_delim}[^${end_delim}]*${end_delim}" | sort -u)
+ if [[ -n "$missing" ]]; then
+ echo "${tr[missing_vars]:-❌ [template] Variables no definidas:}"
+ echo "$missing"
+ return 1
+ fi
+ fi
+
+ echo "$rendered" | $prefix tee "$dest" > /dev/null
+ echo "$(render_msg "${tr[generated]}" "dest=$dest")"
+}
+
+replace_vars() {
+ local line="$1"
+ local item="$2"
+ local start_delim="$3"
+ local end_delim="$4"
+ for key in "${!vars[@]}"; do
+ line="${line//${start_delim}${key}${end_delim}/${vars[$key]}}"
+ done
+ [[ -n "$item" ]] && line="${line//${start_delim}item${end_delim}/$item}"
+ echo "$line"
+}
+
+check_dependencies_template() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in sed tee grep cat; do
+ command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [template] Todas las dependencias están disponibles}"
+ return 0
+}
diff --git a/core/modules/template.tr.en b/core/modules/template.tr.en
new file mode 100644
index 0000000..8c985d8
--- /dev/null
+++ b/core/modules/template.tr.en
@@ -0,0 +1,5 @@
+missing_template=❌ [template] Template not found: {path}
+missing_vars=❌ [template] Undefined variables:
+generated=✅ [template] File generated: {dest}
+missing_deps=❌ [template] Missing dependencies: {cmds}
+deps_ok=✅ [template] All dependencies are available
diff --git a/core/modules/template.tr.es b/core/modules/template.tr.es
new file mode 100644
index 0000000..f9bdff4
--- /dev/null
+++ b/core/modules/template.tr.es
@@ -0,0 +1,5 @@
+missing_template=❌ [template] Plantilla no encontrada: {path}
+missing_vars=❌ [template] Variables no definidas:
+generated=✅ [template] Archivo generado: {dest}
+missing_deps=❌ [template] Dependencias faltantes: {cmds}
+deps_ok=✅ [template] Todas las dependencias están disponibles
diff --git a/core/modules/user.tr.en b/core/modules/user.tr.en
new file mode 100644
index 0000000..9e1d177
--- /dev/null
+++ b/core/modules/user.tr.en
@@ -0,0 +1,15 @@
+priv_required=❌ [users] Privileges required to manage users. Use 'become: true'.
+missing_username=❌ [users] Missing required parameter 'username'
+enter_create=🔧 [users] Entering create
+enter_modify=🔧 [users] Entering modify
+enter_absent=🔧 [users] Entering absent
+exists=✅ [users] User '{username}' already exists
+not_exists=⚠️ [users] User '{username}' does not exist
+group_create=🔧 [users] Creating group '{groups}'
+created=✅ [users] User '{username}' created
+modified=✅ [users] User '{username}' modified
+deleted=✅ [users] User '{username}' deleted
+already_deleted=✅ [users] User '{username}' already deleted
+unsupported_state=❌ [users] State '{state}' not supported. Use create, modify or absent.
+missing_deps=❌ [users] Missing dependencies: {cmds}
+deps_ok=✅ [users] All dependencies are present
diff --git a/core/modules/user.tr.es b/core/modules/user.tr.es
new file mode 100644
index 0000000..1820c74
--- /dev/null
+++ b/core/modules/user.tr.es
@@ -0,0 +1,15 @@
+priv_required=❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'.
+missing_username=❌ [users] Falta el parámetro obligatorio 'username'
+enter_create=🔧 [users] Entrando en create
+enter_modify=🔧 [users] Entrando en modify
+enter_absent=🔧 [users] Entrando en absent
+exists=✅ [users] Usuario '{username}' ya existe
+not_exists=⚠️ [users] Usuario '{username}' no existe
+group_create=🔧 [users] Creando grupo '{groups}'
+created=✅ [users] Usuario '{username}' creado
+modified=✅ [users] Usuario '{username}' modificado
+deleted=✅ [users] Usuario '{username}' eliminado
+already_deleted=✅ [users] Usuario '{username}' ya eliminado
+unsupported_state=❌ [users] Estado '{state}' no soportado. Usa create, modify o absent.
+missing_deps=❌ [users] Dependencias faltantes: {cmds}
+deps_ok=✅ [users] Todas las dependencias están presentes
diff --git a/core/modules/users.sh b/core/modules/users.sh
new file mode 100644
index 0000000..e7306e2
--- /dev/null
+++ b/core/modules/users.sh
@@ -0,0 +1,106 @@
+#!/usr/bin/env bash
+# Module: users
+# Description: Gestiona usuarios del sistema (crear, modificar, eliminar)
+# Author: Luis GuLo
+# Version: 1.4.0
+# Dependencies: id, useradd, usermod, userdel, groupadd, sudo
+
+users_task() {
+ local host="$1"; shift
+ declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local username="${args[username]}"
+ local home="${args[home]:-/home/$username}"
+ local shell="${args[shell]:-/bin/bash}"
+ local groups="${args[groups]:-}"
+ local state="${args[state]:-create}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # 🌐 Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ # 🛡️ Validación
+ if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then
+ echo "${tr[priv_required]:-❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'.}"
+ return 1
+ fi
+
+ if [[ -z "$username" ]]; then
+ echo "${tr[missing_username]:-❌ [users] Falta el parámetro obligatorio 'username'}"
+ return 1
+ fi
+
+ case "$state" in
+ create)
+ echo "${tr[enter_create]:-🔧 [users] Entrando en create}"
+ if id "$username" &>/dev/null; then
+ echo "$(render_msg "${tr[exists]}" "username=$username")"
+ return 0
+ fi
+ if [[ -n "$groups" && "$groups" != "$username" ]]; then
+ if ! getent group "$groups" &>/dev/null; then
+ echo "$(render_msg "${tr[group_create]}" "groups=$groups")"
+ $prefix groupadd "$groups"
+ fi
+ fi
+ local cmd="$prefix useradd -m \"$username\" -s \"$shell\" -d \"$home\""
+ [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\""
+ eval "$cmd" && echo "$(render_msg "${tr[created]}" "username=$username")"
+ ;;
+ modify)
+ echo "${tr[enter_modify]:-🔧 [users] Entrando en modify}"
+ if ! id "$username" &>/dev/null; then
+ echo "$(render_msg "${tr[not_exists]}" "username=$username")"
+ return 1
+ fi
+ local cmd="$prefix usermod \"$username\""
+ [[ -n "$shell" ]] && cmd="$cmd -s \"$shell\""
+ [[ -n "$home" ]] && cmd="$cmd -d \"$home\""
+ [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\""
+ eval "$cmd" && echo "$(render_msg "${tr[modified]}" "username=$username")"
+ ;;
+ absent)
+ echo "${tr[enter_absent]:-🔧 [users] Entrando en absent}"
+ if ! id "$username" &>/dev/null; then
+ echo "$(render_msg "${tr[already_deleted]}" "username=$username")"
+ return 0
+ fi
+ eval "$prefix userdel -r \"$username\"" && echo "$(render_msg "${tr[deleted]}" "username=$username")"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported_state]}" "state=$state")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_users() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ local missing=()
+ for cmd in id sudo; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ missing+=("$cmd")
+ fi
+ done
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")"
+ return 1
+ fi
+
+ echo "${tr[deps_ok]:-✅ [users] Todas las dependencias están presentes}"
+ return 0
+}
diff --git a/core/modules/vault-remote.sh b/core/modules/vault-remote.sh
new file mode 100644
index 0000000..4b3a1c6
--- /dev/null
+++ b/core/modules/vault-remote.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+# Module: vault-remote
+# Description: Sincroniza secretos cifrados entre vault local y remoto
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: ssh, scp, gpg
+
+VAULT_DIR="core/vault"
+
+vault_remote_task() {
+ local host="$1"; shift
+ declare -A args
+ for arg in "$@"; do
+ key="${arg%%=*}"
+ value="${arg#*=}"
+ args["$key"]="$value"
+ done
+
+ local action="${args[action]}"
+ local key="${args[key]}"
+ local remote_path="${args[remote_path]:-/tmp/shflow_vault}"
+ local become="${args[become]}"
+ local prefix=""
+ [ "$become" = "true" ] && prefix="sudo"
+
+ # Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ case "$action" in
+ push)
+ if [ ! -f "$VAULT_DIR/$key.gpg" ]; then
+ echo "$(render_msg "${tr[missing_local]}" "key=$key")"
+ return 1
+ fi
+ scp "$VAULT_DIR/$key.gpg" "$host:$remote_path/$key.gpg"
+ ssh "$host" "$prefix mkdir -p '$remote_path'"
+ echo "$(render_msg "${tr[pushed]}" "key=$key" "host=$host" "path=$remote_path")"
+ ;;
+ pull)
+ ssh "$host" "$prefix test -f '$remote_path/$key.gpg'" || {
+ echo "$(render_msg "${tr[missing_remote]}" "key=$key")"
+ return 1
+ }
+ scp "$host:$remote_path/$key.gpg" "$VAULT_DIR/$key.gpg"
+ echo "$(render_msg "${tr[pulled]}" "key=$key" "host=$host")"
+ ;;
+ sync)
+ ssh "$host" "$prefix mkdir -p '$remote_path'"
+ scp "$VAULT_DIR/"*.gpg "$host:$remote_path/"
+ echo "$(render_msg "${tr[synced]}" "host=$host" "path=$remote_path")"
+ ;;
+ *)
+ echo "$(render_msg "${tr[unsupported]}" "action=$action")"
+ return 1
+ ;;
+ esac
+}
+
+check_dependencies_vault_remote() {
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"
+ fi
+
+ for cmd in ssh scp gpg; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "$(render_msg "${tr[missing_deps]}" "cmd=$cmd")"
+ return 1
+ fi
+ done
+ echo "${tr[deps_ok]:-✅ [vault-remote] Dependencias disponibles.}"
+ return 0
+}
diff --git a/core/modules/vault-remote.tr.en b/core/modules/vault-remote.tr.en
new file mode 100644
index 0000000..cb0b856
--- /dev/null
+++ b/core/modules/vault-remote.tr.en
@@ -0,0 +1,8 @@
+missing_local=❌ [vault-remote] Secret '{key}' does not exist locally.
+missing_remote=❌ [vault-remote] Secret '{key}' does not exist on remote host.
+pushed=📤 Secret '{key}' sent to {host}:{path}
+pulled=📥 Secret '{key}' retrieved from {host}
+synced=🔄 Vault synchronized with {host}:{path}
+unsupported=❌ [vault-remote] Action '{action}' not supported.
+missing_deps=❌ [vault-remote] Command '{cmd}' not available.
+deps_ok=✅ [vault-remote] Dependencies available.
diff --git a/core/modules/vault-remote.tr.es b/core/modules/vault-remote.tr.es
new file mode 100644
index 0000000..79a6106
--- /dev/null
+++ b/core/modules/vault-remote.tr.es
@@ -0,0 +1,8 @@
+missing_local=❌ [vault-remote] Secreto '{key}' no existe localmente.
+missing_remote=❌ [vault-remote] Secreto '{key}' no existe en el host remoto.
+pushed=📤 Secreto '{key}' enviado a {host}:{path}
+pulled=📥 Secreto '{key}' recuperado desde {host}
+synced=🔄 Vault sincronizado con {host}:{path}
+unsupported=❌ [vault-remote] Acción '{action}' no soportada.
+missing_deps=❌ [vault-remote] Comando '{cmd}' no disponible.
+deps_ok=✅ [vault-remote] Dependencias disponibles.
diff --git a/core/modules/wait.sh b/core/modules/wait.sh
new file mode 100644
index 0000000..409dfbe
--- /dev/null
+++ b/core/modules/wait.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# Module: wait
+# Description: Pausa la ejecución durante un número de segundos (soporta decimales)
+# Author: Luis GuLo
+# Version: 1.2.0
+# Dependencies: sleep
+
+wait_task() {
+ local host="$1"; shift
+ declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done
+
+ local seconds="${args[seconds]:-1}"
+
+ # Cargar traducciones
+ local lang="${shflow_vars[language]:-es}"
+ local trfile="$(dirname "${BASH_SOURCE[0]}")/wait.tr.${lang}"
+ declare -A tr
+ if [[ -f "$trfile" ]]; then
+ while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile"
+ fi
+
+ if ! [[ "$seconds" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
+ echo "${tr[invalid]:-❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal)}"
+ return 1
+ fi
+
+ echo "$(render_msg "${tr[start]}" "seconds=$seconds")"
+ sleep "$seconds"
+ echo "${tr[done]:-✅ [wait] Pausa completada}"
+}
+
+check_dependencies_wait() {
+ command -v sleep &>/dev/null || {
+ echo "${tr[missing_deps]:-❌ [wait] El comando 'sleep' no está disponible}"
+ return 1
+ }
+ echo "${tr[deps_ok]:-✅ [wait] Dependencias OK}"
+ return 0
+}
diff --git a/core/modules/wait.tr.en b/core/modules/wait.tr.en
new file mode 100644
index 0000000..a603364
--- /dev/null
+++ b/core/modules/wait.tr.en
@@ -0,0 +1,5 @@
+invalid=❌ [wait] The 'seconds' parameter must be a valid number (integer or decimal)
+start=⏳ [wait] Waiting for {seconds} seconds...
+done=✅ [wait] Pause completed
+missing_deps=❌ [wait] The 'sleep' command is not available
+deps_ok=✅ [wait] Dependencies OK
diff --git a/core/modules/wait.tr.es b/core/modules/wait.tr.es
new file mode 100644
index 0000000..764ed3f
--- /dev/null
+++ b/core/modules/wait.tr.es
@@ -0,0 +1,5 @@
+invalid=❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal)
+start=⏳ [wait] Esperando {seconds} segundos...
+done=✅ [wait] Pausa completada
+missing_deps=❌ [wait] El comando 'sleep' no está disponible
+deps_ok=✅ [wait] Dependencias OK
diff --git a/core/templates/.gitignore b/core/templates/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/templates/.gitignore
diff --git a/core/utils/eg.sh b/core/utils/eg.sh
new file mode 100755
index 0000000..551c9d0
--- /dev/null
+++ b/core/utils/eg.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+Qes="8J+lmiBIYXMgZW5jb250cmFkbyBlbCBodWV2byBkZSBwYXNjdWEuIMKhQnVlbiBvam8h·4pyoIE5vIHRvZG9zIGxvcyBzY3JpcHRzIHRpZW5lbiBhbG1hLi4uIHBlcm8gZXN0ZSBzw60u·8J+noCBFbCBtZWpvciBidWcgZXMgZWwgcXVlIG51bmNhIGV4aXN0acOzLg==·8J+QoyBTaEZsb3cgdGUgc2FsdWRhIGRlc2RlIGxhcyBzb21icmFzLg==·8J+TnCBMYSBhdXRvbWF0aXphY2nDs24gdGFtYmnDqW4gdGllbmUgcG9lc8OtYS4="
+Qen="8J+lmiBZb3UgZm91bmQgdGhlIEVhc3RlciBlZ2cuIFNoYXJwIGV5ZSE=·4pyoIE5vdCBhbGwgc2NyaXB0cyBoYXZlIHNvdWwuLi4gYnV0IHRoaXMgb25lIGRvZXMu·8J+noCBUaGUgYmVzdCBidWcgaXMgdGhlIG9uZSB0aGF0IG5ldmVyIGV4aXN0ZWQu·8J+QoyBTaEZsb3cgZ3JlZXRzIHlvdSBmcm9tIHRoZSBzaGFkb3dzLg==·8J+TnCBBdXRvbWF0aW9uIGhhcyBwb2V0cnkgdG9vLg=="
+vhs=([0]="448b55f2" [1]="c9f66247" [2]="154f020f" [3]="0e931208" [4]="d2e2fa57")
+
+sheg() {
+ local P="$1" ; P=$((P+1))
+ echo "――――――"
+ echo "$Qes" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo
+ echo "$Qen" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo
+ echo "――――――"
+}
+
+main() {
+ [[ $# -lt 1 ]] && return 0
+ local input="$1" ; local hsh
+ hsh=$(echo -n "$input" | md5sum | cut -c1-8)
+ for n in "${!vhs[@]}"; do
+ if [[ "$hsh" == "${vhs[$n]}" ]]; then
+ sheg "$n" ; break
+ fi
+ done
+ return 0
+}
+
+main "$@"
diff --git a/core/utils/module-docgen.sh b/core/utils/module-docgen.sh
new file mode 100755
index 0000000..f5461cd
--- /dev/null
+++ b/core/utils/module-docgen.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+# ShFlow Module Documentation Generator
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.4.0
+
+set -euo pipefail
+
+# 🧭 Detección de la raíz del proyecto
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+OUTPUT="$PROJECT_ROOT/docs/modules-list.md"
+MODULE_DIRS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules")
+
+export SHFLOW_LANG="${SHFLOW_LANG:-es}"
+
+# 🧩 Cargar render_msg si no está disponible
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+
+trfile="$PROJECT_ROOT/core/utils/module-docgen.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+# 📝 Encabezado del documento
+{
+ echo "${tr[title]:-# 🧩 Módulos en ShFlow}"
+ echo ""
+ echo "**$(render_msg "${tr[generated]}" "date=$(date '+%Y-%m-%d %H:%M:%S')")**"
+ echo ""
+ echo "| ${tr[col_module]:-Módulo} | ${tr[col_desc]:-Descripción} | ${tr[col_type]:-Tipo} | ${tr[col_author]:-Autor} | ${tr[col_version]:-Versión} | ${tr[col_deps]:-Dependencias} |"
+ echo "|--------|-------------|------|-------|---------|--------------|"
+} > "$OUTPUT"
+
+# 🔁 Procesar módulos
+for dir in "${MODULE_DIRS[@]}"; do
+ [ -d "$dir" ] || continue
+ TYPE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g")
+ while IFS= read -r -d '' file; do
+ name=$(basename "$file" .sh)
+ desc=$(grep -E '^# Description:' "$file" | sed 's/^# Description:[[:space:]]*//')
+ author=$(grep -E '^# Author:' "$file" | sed 's/^# Author:[[:space:]]*//')
+ version=$(grep -E '^# Version:' "$file" | sed 's/^# Version:[[:space:]]*//')
+ deps=$(grep -E '^# Dependencies:' "$file" | sed 's/^# Dependencies:[[:space:]]*//')
+
+ # Asegurar valor minimo
+ name=${name:-""}
+ desc=${desc:-""}
+ author=${author:-""}
+ version=${version:-""}
+ deps=${deps:-""}
+
+ [[ -z "$name" ]] && continue
+
+ echo "| $name | $desc | $TYPE | $author | $version | $deps |" >> "$OUTPUT"
+ done < <(find "$dir" -type f -name "*.sh" -print0)
+done
+
+# 📌 Pie de página
+{
+ echo ""
+ echo "${tr[footer]:-_Para actualizar esta tabla, ejecuta: \`module-docgen\`_}"
+} >> "$OUTPUT"
+
+echo "$(render_msg "${tr[done]}" "path=$OUTPUT")"
diff --git a/core/utils/module-docgen.tr.en b/core/utils/module-docgen.tr.en
new file mode 100644
index 0000000..644b2c3
--- /dev/null
+++ b/core/utils/module-docgen.tr.en
@@ -0,0 +1,10 @@
+title=# 🧩 Modules in ShFlow
+generated=Automatically generated on {date}
+col_module=Module
+col_desc=Description
+col_type=Type
+col_author=Author
+col_version=Version
+col_deps=Dependencies
+footer=_To update this table, run: `module-docgen`_
+done=✅ Documentation generated at {path}
diff --git a/core/utils/module-docgen.tr.es b/core/utils/module-docgen.tr.es
new file mode 100644
index 0000000..14b68fa
--- /dev/null
+++ b/core/utils/module-docgen.tr.es
@@ -0,0 +1,10 @@
+title=# 🧩 Módulos en ShFlow
+generated=Generado automáticamente el {date}
+col_module=Módulo
+col_desc=Descripción
+col_type=Tipo
+col_author=Autor
+col_version=Versión
+col_deps=Dependencias
+footer=_Para actualizar esta tabla, ejecuta: `module-docgen`_
+done=✅ Documentación generada en {path}
diff --git a/core/utils/module-template.sh b/core/utils/module-template.sh
new file mode 100755
index 0000000..a38624b
--- /dev/null
+++ b/core/utils/module-template.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+# ShFlow Module Template Generator
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+
+set -euo pipefail
+
+# 📁 Rutas defensivas
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+MODULE_NAME="${1:-}"
+MODULE_DIR="$PROJECT_ROOT/core/modules"
+MODULE_FILE="$MODULE_DIR/$MODULE_NAME.sh"
+
+# 🧩 Cargar render_msg si no está disponible
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+trfile="$PROJECT_ROOT/core/utils/module-template.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+# 🧪 Validar entrada
+if [[ -z "$MODULE_NAME" ]]; then
+ echo "${tr[usage]:-❌ Uso: module-template.sh <nombre_modulo>}"
+ exit 1
+fi
+
+if [[ -f "$MODULE_FILE" ]]; then
+ echo "$(render_msg "${tr[exists]}" "name=$MODULE_NAME" "dir=$MODULE_DIR")"
+ exit 1
+fi
+
+mkdir -p "$MODULE_DIR"
+
+cat > "$MODULE_FILE" <<EOF
+#!/bin/bash
+# Module: $MODULE_NAME
+# Description: <descripción breve del módulo>
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.0
+# Dependencies: <comandos externos si aplica>
+
+${MODULE_NAME}_task() {
+ local host="\$1"; shift
+ declare -A args
+ for arg in "\$@"; do
+ key="\${arg%%=*}"
+ value="\${arg#*=}"
+ args["\$key"]="\$value"
+ done
+
+ echo "🚧 Ejecutando módulo '$MODULE_NAME' en \$host"
+ # Aquí va la lógica principal
+}
+
+check_dependencies_$MODULE_NAME() {
+ # Verifica herramientas necesarias
+ for cmd in <comando1> <comando2>; do
+ if ! command -v "\$cmd" &> /dev/null; then
+ echo " ❌ [$MODULE_NAME] Falta: \$cmd"
+ return 1
+ fi
+ done
+ echo " ✅ [$MODULE_NAME] Dependencias OK"
+ return 0
+}
+EOF
+
+chmod +x "$MODULE_FILE"
+echo "$(render_msg "${tr[created]}" "name=$MODULE_NAME" "path=$MODULE_FILE")"
diff --git a/core/utils/module-template.tr.en b/core/utils/module-template.tr.en
new file mode 100644
index 0000000..bc948d7
--- /dev/null
+++ b/core/utils/module-template.tr.en
@@ -0,0 +1,3 @@
+usage=❌ Usage: module-template.sh <module_name>
+exists=⚠️ Module '{name}' already exists in {dir}
+created=✅ Module '{name}' created at {path}
diff --git a/core/utils/module-template.tr.es b/core/utils/module-template.tr.es
new file mode 100644
index 0000000..4dc6e63
--- /dev/null
+++ b/core/utils/module-template.tr.es
@@ -0,0 +1,3 @@
+usage=❌ Uso: module-template.sh <nombre_modulo>
+exists=⚠️ El módulo '{name}' ya existe en {dir}
+created=✅ Módulo '{name}' creado en {path}
diff --git a/core/utils/shflow-check.sh b/core/utils/shflow-check.sh
new file mode 100755
index 0000000..130eca0
--- /dev/null
+++ b/core/utils/shflow-check.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+# ShFlow Environment Checker
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.5.0
+
+set -e
+
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+
+MODULE_PATHS=(
+ "$PROJECT_ROOT/core/modules"
+ "$PROJECT_ROOT/user_modules"
+ "$PROJECT_ROOT/community_modules"
+)
+
+# 🧩 Cargar funciones comunes si no están disponibles
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+GLOBAL_TOOLS=("bash" "ssh" "scp" "git" "curl" "jq" "yq" "gpg")
+
+REQUIRED_PATHS=(
+ "$PROJECT_ROOT/core/modules"
+ "$PROJECT_ROOT/core/utils"
+ "$PROJECT_ROOT/core/inventory"
+ "$PROJECT_ROOT/examples"
+ "$PROJECT_ROOT/user_modules"
+ "$PROJECT_ROOT/community_modules"
+ "$PROJECT_ROOT/shflow.sh"
+ "$PROJECT_ROOT/vault.sh"
+)
+
+# 🌐 Cargar traducciones
+lang="${shflow_vars[language]:-es}"
+trfile="$PROJECT_ROOT/core/utils/shflow-check.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+check_global_tools() {
+ echo "${tr[tools_header]:-🔍 Verificando herramientas globales...}"
+ local missing=0
+ for tool in "${GLOBAL_TOOLS[@]}"; do
+ if ! command -v "$tool" &> /dev/null; then
+ echo "$(render_msg "${tr[tool_missing]}" "tool=$tool")"
+ missing=1
+ else
+ echo "$(render_msg "${tr[tool_ok]}" "tool=$tool")"
+ fi
+ done
+ return $missing
+}
+
+check_structure() {
+ echo ""
+ echo "${tr[structure_header]:-📁 Verificando estructura de ShFlow...}"
+ local missing=0
+ for path in "${REQUIRED_PATHS[@]}"; do
+ if [ ! -e "$path" ]; then
+ echo "$(render_msg "${tr[path_missing]}" "path=$path")"
+ missing=1
+ else
+ echo "$(render_msg "${tr[path_ok]}" "path=$path")"
+ fi
+ done
+ return $missing
+}
+
+load_and_check_modules() {
+ echo ""
+ echo "${tr[modules_header]:-🔍 Verificando módulos ShFlow...}"
+ for dir in "${MODULE_PATHS[@]}"; do
+ [ -d "$dir" ] || continue
+ while IFS= read -r -d '' mod; do
+ source "$mod"
+ done < <(find "$dir" -type f -name "*.sh" -print0)
+ done
+
+ for func in $(declare -F | awk '{print $3}' | grep '^check_dependencies_'); do
+ echo ""
+ echo "$(render_msg "${tr[checking_func]}" "func=$func")"
+ $func || echo "$(render_msg "${tr[func_warn]}" "func=$func")"
+ done
+}
+
+main() {
+ echo "${tr[title]:-🧪 ShFlow Environment Check}"
+ echo "${tr[separator]:-=============================}"
+
+ check_global_tools
+ check_structure
+ load_and_check_modules
+
+ echo ""
+ echo "${tr[done]:-✅ Verificación completada.}"
+}
+
+main "$@"
diff --git a/core/utils/shflow-check.tr.en b/core/utils/shflow-check.tr.en
new file mode 100644
index 0000000..6430ffd
--- /dev/null
+++ b/core/utils/shflow-check.tr.en
@@ -0,0 +1,12 @@
+title=🧪 ShFlow Environment Check
+separator=_____________________________
+tools_header=🔍 Checking global tools...
+tool_missing=❌ {tool} not found
+tool_ok=✅ {tool} available
+structure_header=📁 Checking ShFlow structure...
+path_missing=❌ Missing: {path}
+path_ok=✅ Found: {path}
+modules_header=🔍 Checking ShFlow modules...
+checking_func=🔧 Running {func}...
+func_warn=⚠️ Incomplete dependencies in {func}
+done=✅ Check completed.
diff --git a/core/utils/shflow-check.tr.es b/core/utils/shflow-check.tr.es
new file mode 100644
index 0000000..983f318
--- /dev/null
+++ b/core/utils/shflow-check.tr.es
@@ -0,0 +1,12 @@
+title=🧪 ShFlow Environment Check
+separator=_____________________________
+tools_header=🔍 Verificando herramientas globales...
+tool_missing=❌ {tool} no encontrado
+tool_ok=✅ {tool} disponible
+structure_header=📁 Verificando estructura de ShFlow...
+path_missing=❌ Falta: {path}
+path_ok=✅ Encontrado: {path}
+modules_header=🔍 Verificando módulos ShFlow...
+checking_func=🔧 Ejecutando {func}...
+func_warn=⚠️ Dependencias incompletas en {func}
+done=✅ Verificación completada.
diff --git a/core/utils/shflow-doc.sh b/core/utils/shflow-doc.sh
new file mode 100755
index 0000000..861f1b9
--- /dev/null
+++ b/core/utils/shflow-doc.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# ShFlow Doc Generator
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+
+set -e
+
+# ─────────────────────────────────────────────
+# 🧭 Detección de la raíz del proyecto
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+
+# 🧩 Cargar funciones comunes si no están disponibles
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+# 🌐 Cargar traducciones
+lang="${shflow_vars[language]:-es}"
+trfile="$PROJECT_ROOT/core/utils/shflow-doc.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+MODULE_PATHS=(
+ "$PROJECT_ROOT/core/modules"
+ "$PROJECT_ROOT/user_modules"
+ "$PROJECT_ROOT/community_modules"
+)
+
+extract_metadata() {
+ local file="$1"
+ local module desc author version deps
+
+ module=$(grep -m1 '^# Module:' "$file" | cut -d':' -f2- | xargs)
+ desc=$(grep -m1 '^# Description:' "$file" | cut -d':' -f2- | xargs)
+ author=$(grep -m1 '^# Author:' "$file" | cut -d':' -f2- | xargs)
+ version=$(grep -m1 '^# Version:' "$file" | cut -d':' -f2- | xargs)
+ deps=$(grep -m1 '^# Dependencies:' "$file" | cut -d':' -f2- | xargs)
+
+ echo "$(render_msg "${tr[module]}" "name=$module")"
+ echo "$(render_msg "${tr[desc]}" "desc=$desc")"
+ echo "$(render_msg "${tr[author]}" "author=$author")"
+ echo "$(render_msg "${tr[version]}" "version=$version")"
+ echo "$(render_msg "${tr[deps]}" "deps=$deps")"
+ echo "${tr[separator]:- ————————————————————————}"
+}
+
+main() {
+ echo "${tr[title]:-📚 ShFlow Modules Documentation}"
+ echo "${tr[separator_line]:-=================================}"
+
+ for dir in "${MODULE_PATHS[@]}"; do
+ [ -d "$dir" ] || continue
+ ROUTE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g")
+ echo -e "\n$(render_msg "${tr[section]}" "type=$ROUTE")"
+ while IFS= read -r -d '' file; do
+ extract_metadata "$file"
+ done < <(find "$dir" -type f -name "*.sh" -print0)
+ done
+}
+
+main "$@"
diff --git a/core/utils/shflow-doc.tr.en b/core/utils/shflow-doc.tr.en
new file mode 100644
index 0000000..5d3167e
--- /dev/null
+++ b/core/utils/shflow-doc.tr.en
@@ -0,0 +1,9 @@
+title=📚 ShFlow Modules Documentation
+separator_line==================================
+section=🗃️ Module Type: {type}
+module= 📦 Module: {name}
+desc= 🔧 Description: {desc}
+author= 👤 Author: {author}
+version= 📌 Version: {version}
+deps= 📎 Dependencies: {deps}
+separator= ————————————————————————
diff --git a/core/utils/shflow-doc.tr.es b/core/utils/shflow-doc.tr.es
new file mode 100644
index 0000000..b1150f7
--- /dev/null
+++ b/core/utils/shflow-doc.tr.es
@@ -0,0 +1,9 @@
+title=📚 ShFlow Modules Documentation
+separator_line==================================
+section=🗃️ Tipo de módulo: {type}
+module= 📦 Módulo: {name}
+desc= 🔧 Descripción: {desc}
+author= 👤 Autor: {author}
+version= 📌 Versión: {version}
+deps= 📎 Dependencias: {deps}
+separator= ————————————————————————
diff --git a/core/utils/shflow-ssh-init.sh b/core/utils/shflow-ssh-init.sh
new file mode 100755
index 0000000..26d5e70
--- /dev/null
+++ b/core/utils/shflow-ssh-init.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+# Utility: shflow-ssh-init
+# Description: Inicializa acceso SSH sin contraseña en los hosts del inventario
+# Author: Luis GuLo
+# Version: 0.2.0
+
+set -euo pipefail
+
+# 📁 Rutas defensivas
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
+INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml"
+TIMEOUT=5
+USER="${USER:-$(whoami)}"
+KEY="${KEY:-$HOME/.ssh/id_rsa.pub}"
+
+# 🧩 Cargar render_msg si no está disponible
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+export SHFLOW_LANG="${SHFLOW_LANG:-es}"
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+
+trfile="$PROJECT_ROOT/core/utils/shflow-ssh-init.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+echo "$(render_msg "${tr[start]}" "user=$USER")"
+echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")"
+echo "$(render_msg "${tr[key]}" "key=$KEY")"
+echo ""
+
+# 🧪 Validar dependencias
+for cmd in yq ssh ssh-copy-id; do
+ if ! command -v "$cmd" &>/dev/null; then
+ echo "$(render_msg "${tr[missing_dep]}" "cmd=$cmd")"
+ exit 1
+ fi
+done
+
+# 🔁 Extraer hosts
+HOSTS=()
+HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY")
+[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1
+
+while IFS= read -r line; do
+ HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas
+done <<< "$HOSTS_RAW"
+
+# 🔍 Evaluar cada host
+for host in "${HOSTS[@]}"; do
+ IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY")
+ [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue
+
+ echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")"
+
+ if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'true' &>/dev/null; then
+ echo "${tr[skip]:- 🔁 Inicialización SSH no es necesaria}"
+ continue
+ fi
+
+ echo "$(render_msg "${tr[copying]}" "user=$USER" "ip=$IP")"
+ if ssh-copy-id -i "$KEY" "$USER@$IP"; then
+ echo "${tr[success]:- ✅ Clave pública instalada correctamente}"
+ else
+ echo "${tr[fail]:- ❌ Fallo al instalar clave pública}"
+ fi
+
+ echo ""
+done
+
+echo "${tr[done]:-✅ Proceso de inicialización SSH completado}"
diff --git a/core/utils/shflow-ssh-init.tr.en b/core/utils/shflow-ssh-init.tr.en
new file mode 100644
index 0000000..1005b42
--- /dev/null
+++ b/core/utils/shflow-ssh-init.tr.en
@@ -0,0 +1,12 @@
+start=🔐 Initializing passwordless SSH access for user: {user}
+inventory=📁 Inventory: {path}
+key=🔑 Public key: {key}
+missing_dep=❌ Requires '{cmd}' installed on the system
+no_hosts=❌ No hosts found in inventory.
+missing_ip=⚠️ Host '{host}' has no ansible_host defined
+checking=🖥️ Host: {host} ({ip})
+skip= 🔁 SSH initialization not needed
+copying= 🚀 Running ssh-copy-id for {user}@{ip}
+success= ✅ Public key installed successfully
+fail= ❌ Failed to install public key
+done=✅ SSH initialization process completed
diff --git a/core/utils/shflow-ssh-init.tr.es b/core/utils/shflow-ssh-init.tr.es
new file mode 100644
index 0000000..c693c8f
--- /dev/null
+++ b/core/utils/shflow-ssh-init.tr.es
@@ -0,0 +1,12 @@
+start=🔐 Inicializando acceso SSH sin contraseña para usuario: {user}
+inventory=📁 Inventario: {path}
+key=🔑 Clave pública: {key}
+missing_dep=❌ Requiere '{cmd}' instalado en el sistema
+no_hosts=❌ No se encontraron hosts en el inventario.
+missing_ip=⚠️ Host '{host}' sin ansible_host definido
+checking=🖥️ Host: {host} ({ip})
+skip= 🔁 Inicialización SSH no es necesaria
+copying= 🚀 Ejecutando ssh-copy-id para {user}@{ip}
+success= ✅ Clave pública instalada correctamente
+fail= ❌ Fallo al instalar clave pública
+done=✅ Proceso de inicialización SSH completado
diff --git a/core/utils/shflow-trust.sh b/core/utils/shflow-trust.sh
new file mode 100755
index 0000000..bc27d94
--- /dev/null
+++ b/core/utils/shflow-trust.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+# Utility: shflow-trust
+# Description: Evalúa acceso SSH y privilegios sudo para cada host del inventario
+# Author: Luis GuLo
+# Version: 0.4.0
+
+set -euo pipefail
+
+# 📁 Rutas defensivas
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
+INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml"
+REPORT="$PROJECT_ROOT/core/inventory/trust_report.yaml"
+TIMEOUT=5
+USER="${USER:-$(whoami)}"
+
+# 🧩 Cargar render_msg si no está disponible
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+export SHFLOW_LANG="${SHFLOW_LANG:-es}"
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+
+trfile="$PROJECT_ROOT/core/utils/shflow-trust.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+echo "$(render_msg "${tr[start]}" "user=$USER")"
+echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")"
+echo "$(render_msg "${tr[report]}" "path=$REPORT")"
+echo ""
+
+# 🧪 Validar dependencia yq
+if ! command -v yq &>/dev/null; then
+ echo "$(render_msg "${tr[missing_dep]}" "cmd=yq")"
+ exit 1
+fi
+
+# 🧹 Regenerar informe
+{
+ echo "# $(render_msg "${tr[report_title]}")"
+ echo "# $(render_msg "${tr[report_date]}" "date=$(date)")"
+ echo ""
+} > "$REPORT"
+
+# 🔁 Extraer hosts
+HOSTS=()
+HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY")
+[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1
+
+while IFS= read -r line; do
+ HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas
+done <<< "$HOSTS_RAW"
+
+# 🔍 Evaluar cada host
+for host in "${HOSTS[@]}"; do
+ IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY")
+ [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue
+
+ echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")"
+
+ if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'echo ok' &>/dev/null; then
+ echo "${tr[ssh_ok]:- ✅ SSH: ok}"
+ SSH_STATUS="ok"
+
+ if ssh -o BatchMode=yes "$USER@$IP" 'sudo -n true' &>/dev/null; then
+ echo "${tr[sudo_ok]:- ✅ SUDO: ok}"
+ SUDO_STATUS="ok"
+ else
+ echo "${tr[sudo_pw]:- ⚠️ SUDO: requiere contraseña o no permitido}"
+ SUDO_STATUS="password_required"
+ fi
+ else
+ echo "${tr[ssh_fail]:- ❌ SSH: fallo de conexión}"
+ SSH_STATUS="failed"
+ SUDO_STATUS="unknown"
+ fi
+
+ {
+ echo "$host:"
+ echo " ip: $IP"
+ echo " ssh: $SSH_STATUS"
+ echo " sudo: $SUDO_STATUS"
+ echo ""
+ } >> "$REPORT"
+done
+
+echo "$(render_msg "${tr[done]}" "path=$REPORT")"
diff --git a/core/utils/shflow-trust.tr.en b/core/utils/shflow-trust.tr.en
new file mode 100644
index 0000000..5e1209f
--- /dev/null
+++ b/core/utils/shflow-trust.tr.en
@@ -0,0 +1,14 @@
+start=🔍 Evaluating SSH and sudo trust for user: {user}
+inventory=📁 Inventory: {path}
+report=📄 Report: {path}
+missing_dep=❌ Requires '{cmd}' (Go version) to process YAML inventory
+report_title=Trust report generated by shflow-trust
+report_date=Date: {date}
+no_hosts=❌ No hosts found in inventory.
+missing_ip=⚠️ Host '{host}' has no ansible_host defined
+checking=🖥️ Host: {host} ({ip})
+ssh_ok= ✅ SSH: ok
+ssh_fail= ❌ SSH: connection failed
+sudo_ok= ✅ SUDO: ok
+sudo_pw= ⚠️ SUDO: requires password or not allowed
+done=✅ Report completed: {path}
diff --git a/core/utils/shflow-trust.tr.es b/core/utils/shflow-trust.tr.es
new file mode 100644
index 0000000..9e930a9
--- /dev/null
+++ b/core/utils/shflow-trust.tr.es
@@ -0,0 +1,14 @@
+start=🔍 Evaluando confianza SSH y sudo para usuario: {user}
+inventory=📁 Inventario: {path}
+report=📄 Informe: {path}
+missing_dep=❌ Requiere '{cmd}' versión Go para procesar el inventario YAML
+report_title=Informe de confianza generado por shflow-trust
+report_date=Fecha: {date}
+no_hosts=❌ No se encontraron hosts en el inventario.
+missing_ip=⚠️ Host '{host}' sin ansible_host definido
+checking=🖥️ Host: {host} ({ip})
+ssh_ok= ✅ SSH: ok
+ssh_fail= ❌ SSH: fallo de conexión
+sudo_ok= ✅ SUDO: ok
+sudo_pw= ⚠️ SUDO: requiere contraseña o no permitido
+done=✅ Informe completado: {path}
diff --git a/core/utils/vault-init.sh b/core/utils/vault-init.sh
new file mode 100755
index 0000000..3055a10
--- /dev/null
+++ b/core/utils/vault-init.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+# ShFlow Vault Initializer
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.3.0
+# Dependencies: gpg
+
+set -euo pipefail
+
+# 📁 Rutas defensivas
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
+VAULT_DIR="$PROJECT_ROOT/core/vault"
+VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}"
+VAULT_PUBKEY="${VAULT_PUBKEY:-$HOME/.shflow.pub}"
+
+# 🧩 Cargar render_msg si no está disponible
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+trfile="$PROJECT_ROOT/core/utils/vault-init.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+generate_key() {
+ echo "${tr[gen_key]:-🔐 Generando nueva clave simétrica...}"
+ head -c 64 /dev/urandom | base64 > "$VAULT_KEY"
+ chmod 600 "$VAULT_KEY"
+ echo "$(render_msg "${tr[key_created]}" "path=$VAULT_KEY")"
+}
+
+rotate_key() {
+ echo "${tr[rotate_start]:-🔄 Rotando clave y re-cifrando secretos...}"
+ local OLD_KEY="$VAULT_KEY.old"
+
+ cp "$VAULT_KEY" "$OLD_KEY"
+ generate_key
+
+ for file in "$VAULT_DIR"/*.gpg; do
+ key=$(basename "$file" .gpg)
+ echo "$(render_msg "${tr[recrypt]}" "key=$key")"
+ gpg --quiet --batch --yes --passphrase-file "$OLD_KEY" -d "$file" | \
+ gpg --symmetric --batch --yes --passphrase-file "$VAULT_KEY" -o "$VAULT_DIR/$key.gpg.new"
+ mv "$VAULT_DIR/$key.gpg.new" "$VAULT_DIR/$key.gpg"
+ done
+
+ echo "$(render_msg "${tr[rotate_done]}" "path=$OLD_KEY")"
+}
+
+status() {
+ echo "${tr[status_title]:-📊 Estado del Vault}"
+ echo "-------------------"
+ echo "$(render_msg "${tr[sym_key]}" "status=$( [ -f "$VAULT_KEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")"
+ echo "$(render_msg "${tr[pub_key]}" "status=$( [ -f "$VAULT_PUBKEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")"
+ echo "$(render_msg "${tr[vault_path]}" "path=$VAULT_DIR")"
+ echo "$(render_msg "${tr[secrets]}" "count=$(ls "$VAULT_DIR"/*.gpg 2>/dev/null | wc -l)")"
+ echo "$(render_msg "${tr[last_mod]}" "date=$(date -r "$VAULT_KEY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)")"
+}
+
+generate_pubkey() {
+ echo "${tr[asym_start]:-🔐 Configurando cifrado asimétrico...}"
+ echo "${tr[asym_hint]:-⚠️ Se requiere que la clave pública esté exportada previamente.}"
+ echo " gpg --export -a 'usuario@dominio' > $VAULT_PUBKEY"
+ if [ -f "$VAULT_PUBKEY" ]; then
+ echo "$(render_msg "${tr[pubkey_found]}" "path=$VAULT_PUBKEY")"
+ else
+ echo "${tr[pubkey_missing]:-❌ Clave pública no encontrada. Exporta primero con GPG.}"
+ exit 1
+ fi
+}
+
+main() {
+ case "${1:-}" in
+ --rotate)
+ [ -f "$VAULT_KEY" ] || { echo "${tr[no_key]:-❌ No existe clave actual. Ejecuta sin --rotate primero.}"; exit 1; }
+ rotate_key
+ ;;
+ --status)
+ status
+ ;;
+ --asymmetric)
+ generate_pubkey
+ ;;
+ *)
+ if [ -f "$VAULT_KEY" ]; then
+ echo "$(render_msg "${tr[key_exists]}" "path=$VAULT_KEY")"
+ else
+ generate_key
+ fi
+ ;;
+ esac
+}
+
+main "$@"
diff --git a/core/utils/vault-init.tr.en b/core/utils/vault-init.tr.en
new file mode 100644
index 0000000..8004348
--- /dev/null
+++ b/core/utils/vault-init.tr.en
@@ -0,0 +1,19 @@
+gen_key=🔐 Generating new symmetric key...
+key_created=✅ Key created at {path}
+rotate_start=🔄 Rotating key and re-encrypting secrets...
+recrypt=🔁 Re-encrypting '{key}'...
+rotate_done=✅ Rotation completed. Old key saved at {path}
+status_title=📊 Vault Status
+sym_key=🔐 Symmetric key: {status}
+pub_key=🔐 Public key: {status}
+present=✅ present
+absent=❌ absent
+vault_path=📁 Vault path: {path}
+secrets=📦 Secrets: {count}
+last_mod=🕒 Last modified: {date}
+asym_start=🔐 Setting up asymmetric encryption...
+asym_hint=⚠️ Public key must be exported beforehand.
+pubkey_found=✅ Public key found at {path}
+pubkey_missing=❌ Public key not found. Export it first using GPG.
+no_key=❌ No current key found. Run without --rotate first.
+key_exists=🔐 Key already exists at {path}
diff --git a/core/utils/vault-init.tr.es b/core/utils/vault-init.tr.es
new file mode 100644
index 0000000..8ed9644
--- /dev/null
+++ b/core/utils/vault-init.tr.es
@@ -0,0 +1,19 @@
+gen_key=🔐 Generando nueva clave simétrica...
+key_created=✅ Clave creada en {path}
+rotate_start=🔄 Rotando clave y re-cifrando secretos...
+recrypt=🔁 Re-cifrando '{key}'...
+rotate_done=✅ Rotación completada. Clave antigua guardada en {path}
+status_title=📊 Estado del Vault
+sym_key=🔐 Clave simétrica: {status}
+pub_key=🔐 Clave pública: {status}
+present=✅ presente
+absent=❌ ausente
+vault_path=📁 Ruta del vault: {path}
+secrets=📦 Secretos: {count}
+last_mod=🕒 Última modificación: {date}
+asym_start=🔐 Configurando cifrado asimétrico...
+asym_hint=⚠️ Se requiere que la clave pública esté exportada previamente.
+pubkey_found=✅ Clave pública detectada en {path}
+pubkey_missing=❌ Clave pública no encontrada. Exporta primero con GPG.
+no_key=❌ No existe clave actual. Ejecuta sin --rotate primero.
+key_exists=🔐 Clave ya existe en {path}
diff --git a/core/utils/vault_utils.sh b/core/utils/vault_utils.sh
new file mode 100755
index 0000000..702265f
--- /dev/null
+++ b/core/utils/vault_utils.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# Utility: vault_utils
+# Description: Funciones para acceso seguro al vault de ShFlow
+# License: GPLv3
+# Author: Luis GuLo
+# Version: 1.1.0
+# Dependencies: gpg
+
+VAULT_DIR="${VAULT_DIR:-core/vault}"
+VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}"
+
+# 🧩 Cargar render_msg si no está disponible
+PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
+COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh"
+if ! declare -f render_msg &>/dev/null; then
+ [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB"
+fi
+
+# 🌐 Cargar traducciones
+lang="${SHFLOW_LANG:-es}"
+trfile="$PROJECT_ROOT/core/utils/vault_utils.tr.${lang}"
+declare -A tr
+if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi
+
+get_secret() {
+ local key="$1"
+ local value
+
+ if [ ! -f "$VAULT_DIR/$key.gpg" ]; then
+ echo "$(render_msg "${tr[missing]}" "key=$key" "dir=$VAULT_DIR")"
+ return 1
+ fi
+
+ value=$(gpg --quiet --batch --yes --passphrase-file "$VAULT_KEY" -d "$VAULT_DIR/$key.gpg" 2>/dev/null)
+ if [ $? -ne 0 ]; then
+ echo "$(render_msg "${tr[decrypt_fail]}" "key=$key")"
+ return 1
+ fi
+
+ echo "$value"
+}
diff --git a/core/utils/vault_utils.tr.en b/core/utils/vault_utils.tr.en
new file mode 100644
index 0000000..413952e
--- /dev/null
+++ b/core/utils/vault_utils.tr.en
@@ -0,0 +1,2 @@
+missing=❌ [vault] Secret '{key}' not found in {dir}
+decrypt_fail=❌ [vault] Failed to decrypt '{key}'
diff --git a/core/utils/vault_utils.tr.es b/core/utils/vault_utils.tr.es
new file mode 100644
index 0000000..f81ee15
--- /dev/null
+++ b/core/utils/vault_utils.tr.es
@@ -0,0 +1,2 @@
+missing=❌ [vault] Secreto '{key}' no encontrado en {dir}
+decrypt_fail=❌ [vault] Error al descifrar '{key}'
diff --git a/core/vault/.gitignore b/core/vault/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/vault/.gitignore