Files
STP/modules/registry.sh
T
msaldain bd78fd9fbe Configuracion inicial completa del entorno personal
Modulos de restauracion:
- bootstrap: instala yq, age y dependencias base (curl, wget, git, nano, gpg)
- ssh: descifra e instala claves SSH desde secrets/sshKeys.tar.gz.age
- registry: aplica paquetes apt/snap/flatpak, dotfiles, servicios y configs Docker
- thunderbird: instala Thunderbird snap y restaura perfil desde ZIP
- claudeCode: configura repositorio apt de Anthropic e instala claude-code
- easyEffects: restaura configuracion y presets desde ZIP
- wireplumber: restaura dispositivo Bluetooth por defecto y perfiles de audio
- cups: restaura impresoras y drivers PPD desde ZIP

Scripts de captura (correr antes de push):
- scripts/encryptSsh.sh: cifra ~/.ssh con age
- scripts/thunderbird/capture.sh: captura perfil de Thunderbird snap
- scripts/easyEffects/capture.sh: captura config de EasyEffects flatpak
- scripts/wireplumber/capture.sh: captura estado de WirePlumber
- scripts/cups/capture.sh: captura impresoras CUPS y PPDs (requiere sudo)

Registro de aplicaciones (config/registry.yaml):
- 9 paquetes apt, 1 snap (dbeaver-ce), 22 flatpaks incluyendo VSCodium,
  Bitwarden, Inkscape, LibreOffice, OBS Studio, Nextcloud Desktop, entre otros

Secretos incluidos:
- secrets/sshKeys.tar.gz.age: claves SSH cifradas con age
- secrets/thunderbirdProfile.zip: perfil de Thunderbird sin emails ni cache
- secrets/easyEffectsConfig.zip: ajustes y presets de salida de audio
- secrets/wireplumberState.zip: estado de audio incluyendo auriculares Bluetooth
- secrets/cupsConfig.zip: 5 impresoras configuradas con sus drivers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:16:40 -03:00

415 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
source "$stpRoot/lib/log.sh"
source "$stpRoot/lib/utils.sh"
source "$stpRoot/lib/yaml.sh"
readonly registryConfig="$stpRoot/config/registry.yaml"
fieldOf() {
local entryId="$1" field="$2" default="${3:-}"
local value
value="$(yq ".registry[] | select(.id == \"$entryId\") | .${field}" "$registryConfig" 2>/dev/null)"
if util::isYamlNull "$value"; then
echo "$default"
else
echo "$value"
fi
}
arrayFieldOf() {
local entryId="$1" field="$2"
yq -r ".registry[] | select(.id == \"$entryId\") | .${field}[]" "$registryConfig" 2>/dev/null || true
}
# ---- PPA ----
ppaOwnerFrom() {
local ppaAddress="$1"
local owner="${ppaAddress#ppa:}"
echo "${owner%%/*}"
}
ppaIsAlreadyAdded() {
local ppaAddress="$1"
find /etc/apt/sources.list.d/ -name "*$(ppaOwnerFrom "$ppaAddress")*" 2>/dev/null | grep -q .
}
applyPpa() {
local entryId="$1"
local ppaAddress
ppaAddress="$(fieldOf "$entryId" "address")"
if ppaIsAlreadyAdded "$ppaAddress"; then
log::info "PPA ya existe: $ppaAddress"
return
fi
util::cmdExists add-apt-repository \
|| sudo apt-get install -y software-properties-common
sudo add-apt-repository -y "$ppaAddress"
sudo apt-get update -qq
log::ok "PPA agregado: $ppaAddress"
}
# ---- APT ----
installedVersionOf() {
local packageName="$1"
dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null
}
aptVersionIsInRange() {
local installedVersion="$1" minVersion="$2" maxVersion="$3"
if [[ -n "$minVersion" ]] && dpkg --compare-versions "$installedVersion" lt "$minVersion"; then
return 1
fi
if [[ -n "$maxVersion" ]] && dpkg --compare-versions "$installedVersion" gt "$maxVersion"; then
return 1
fi
return 0
}
applyAptPackage() {
local entryId="$1"
local packageName minVersion maxVersion
packageName="$(fieldOf "$entryId" "package" "$entryId")"
minVersion="$(fieldOf "$entryId" "minVersion")"
maxVersion="$(fieldOf "$entryId" "maxVersion")"
if util::isAptInstalled "$packageName"; then
local installedVersion
installedVersion="$(installedVersionOf "$packageName")"
if aptVersionIsInRange "$installedVersion" "$minVersion" "$maxVersion"; then
log::info "Ya instalado (apt): $packageName ($installedVersion)"
return
fi
log::warn "$packageName: versión $installedVersion instalada, fuera del rango requerido"
fi
util::aptUpdateOnce
if [[ -n "$maxVersion" ]]; then
sudo apt-get install -y --allow-downgrades "${packageName}=${maxVersion}"
log::ok "Instalado (apt): ${packageName}=${maxVersion}"
else
sudo apt-get install -y "$packageName"
log::ok "Instalado (apt): $packageName"
fi
}
# ---- SNAP ----
applySnapPackage() {
local entryId="$1"
local packageName classicFlag installFlags
packageName="$(fieldOf "$entryId" "package" "$entryId")"
classicFlag="$(fieldOf "$entryId" "classic" "false")"
installFlags=()
[[ "$classicFlag" == "true" ]] && installFlags+=(--classic)
if util::isSnapInstalled "$packageName"; then
log::info "Ya instalado (snap): $packageName"
return
fi
if ! util::cmdExists snap; then
log::warn "snap no disponible, salteando: $entryId"
return
fi
sudo snap install "$packageName" "${installFlags[@]}"
log::ok "Instalado (snap): $packageName"
}
# ---- FLATPAK ----
applyFlatpakApp() {
local entryId="$1"
local appId remote
appId="$(fieldOf "$entryId" "appId" "$entryId")"
remote="$(fieldOf "$entryId" "remote" "flathub")"
if util::isFlatpakInstalled "$appId"; then
log::info "Ya instalado (flatpak): $appId"
return
fi
if ! util::cmdExists flatpak; then
log::warn "flatpak no disponible, salteando: $entryId"
return
fi
flatpak install -y "$remote" "$appId"
log::ok "Instalado (flatpak): $appId"
}
# ---- DOTFILE ----
dotfileSymlinkAlreadyExists() {
local destination="$1"
[[ -L "$destination" ]]
}
backupExistingFile() {
local destination="$1"
[[ -e "$destination" ]] || return 0
mv "$destination" "${destination}.stpbackup"
log::info "Copia de seguridad: ~/${destination#${HOME}/}"
}
deployFileFromElement() {
local sourceFile="$1" elementSourceDirectory="$2"
local relativePath destination
relativePath="${sourceFile#${elementSourceDirectory}/}"
destination="$HOME/$relativePath"
if dotfileSymlinkAlreadyExists "$destination"; then
log::info "Enlace ya existe: ~/$relativePath"
return
fi
backupExistingFile "$destination"
mkdir -p "$(dirname "$destination")"
ln -sfn "$sourceFile" "$destination"
log::ok "Enlazado: ~/$relativePath"
}
applyDotfile() {
local entryId="$1"
local elementSourceDirectory="$stpRoot/dotfiles/$entryId"
if [[ ! -d "$elementSourceDirectory" ]]; then
log::warn "Carpeta no encontrada: dotfiles/$entryId/"
return
fi
while IFS= read -r sourceFile; do
deployFileFromElement "$sourceFile" "$elementSourceDirectory"
done < <(find "$elementSourceDirectory" -not -type d | sort)
}
# ---- SERVICE ----
applyService() {
local entryId="$1"
local serviceName scope state
serviceName="$(fieldOf "$entryId" "name" "$entryId")"
scope="$(fieldOf "$entryId" "scope" "system")"
state="$(fieldOf "$entryId" "state" "enable")"
local scopeFlags=()
[[ "$scope" == "user" ]] && scopeFlags+=(--user)
case "$state" in
enable)
systemctl "${scopeFlags[@]}" enable --now "$serviceName" 2>/dev/null \
|| log::warn "No se pudo habilitar: $serviceName"
;;
disable)
systemctl "${scopeFlags[@]}" disable --now "$serviceName" 2>/dev/null || true
;;
mask)
systemctl "${scopeFlags[@]}" mask "$serviceName" 2>/dev/null || true
;;
esac
log::ok "Servicio [$scope] $state: $serviceName"
}
# ---- PIPEWIRE ----
allPipewirePackagesAreInstalled() {
local entryId="$1"
while IFS= read -r packageName; do
util::isYamlNull "$packageName" && continue
util::isAptInstalled "$packageName" || return 1
done < <(arrayFieldOf "$entryId" "packages")
}
installMissingPipewirePackages() {
local entryId="$1"
local missingPackages=()
while IFS= read -r packageName; do
util::isYamlNull "$packageName" && continue
util::isAptInstalled "$packageName" || missingPackages+=("$packageName")
done < <(arrayFieldOf "$entryId" "packages")
if [[ ${#missingPackages[@]} -gt 0 ]]; then
util::aptUpdateOnce
sudo apt-get install -y "${missingPackages[@]}"
fi
}
disablePulseaudio() {
systemctl --user --now disable pulseaudio.service pulseaudio.socket 2>/dev/null || true
systemctl --user mask pulseaudio 2>/dev/null || true
}
enablePipewireUserServices() {
local entryId="$1"
while IFS= read -r serviceName; do
util::isYamlNull "$serviceName" && continue
systemctl --user --now enable "$serviceName" 2>/dev/null \
|| log::warn "No se pudo habilitar servicio de usuario: $serviceName"
done < <(arrayFieldOf "$entryId" "userServices")
}
applyPipewire() {
local entryId="$1"
allPipewirePackagesAreInstalled "$entryId" \
|| installMissingPipewirePackages "$entryId"
local shouldReplace
shouldReplace="$(fieldOf "$entryId" "replacePulseaudio" "false")"
[[ "$shouldReplace" == "true" ]] && disablePulseaudio
enablePipewireUserServices "$entryId"
log::ok "PipeWire configurado"
}
# ---- VIDEO ----
detectGpuVendor() {
if lspci 2>/dev/null | grep -qi nvidia; then echo "nvidia"
elif lspci 2>/dev/null | grep -qi "amd\|radeon"; then echo "amd"
elif lspci 2>/dev/null | grep -qi intel; then echo "intel"
else echo "unknown"
fi
}
applyVideoDrivers() {
local entryId="$1"
local gpuVendor
gpuVendor="$(detectGpuVendor)"
log::info "GPU detectada: $gpuVendor"
if [[ "$gpuVendor" == "unknown" ]]; then
log::warn "No se pudo identificar la GPU, salteando drivers"
return
fi
local missingPackages=()
while IFS= read -r packageName; do
util::isYamlNull "$packageName" && continue
util::isAptInstalled "$packageName" || missingPackages+=("$packageName")
done < <(arrayFieldOf "$entryId" "drivers.${gpuVendor}.packages")
if [[ ${#missingPackages[@]} -eq 0 ]]; then
log::info "Drivers $gpuVendor ya instalados"
return
fi
util::aptUpdateOnce
sudo apt-get install -y "${missingPackages[@]}"
log::ok "Drivers $gpuVendor instalados"
}
# ---- DOCKER ----
dockerIsInstalled() {
util::cmdExists docker
}
findDockerComposeFile() {
local destinationDirectory="$1"
local composeFile
for composeFile in "compose.yaml" "compose.yml" "docker-compose.yaml" "docker-compose.yml"; do
[[ -f "$destinationDirectory/$composeFile" ]] && echo "$destinationDirectory/$composeFile" && return 0
done
return 1
}
deployDockerConfigFile() {
local sourceFile="$1" sourceDirectory="$2" destinationDirectory="$3"
local relativePath destination
relativePath="${sourceFile#${sourceDirectory}/}"
destination="$destinationDirectory/$relativePath"
if [[ -L "$destination" ]]; then
log::info "Enlace ya existe: $relativePath"
return
fi
[[ -e "$destination" ]] && mv "$destination" "${destination}.stpbackup"
mkdir -p "$(dirname "$destination")"
ln -sfn "$sourceFile" "$destination"
log::ok "Enlazado: $relativePath"
}
applyDocker() {
local entryId="$1"
local rawDestination destination autostart
rawDestination="$(fieldOf "$entryId" "destination" "~/docker/$entryId")"
destination="${rawDestination/#\~/$HOME}"
autostart="$(fieldOf "$entryId" "autostart" "false")"
local sourceDirectory="$stpRoot/docker/$entryId"
if [[ ! -d "$sourceDirectory" ]]; then
log::warn "Carpeta no encontrada: docker/$entryId/"
return
fi
if ! dockerIsInstalled; then
log::warn "Docker no instalado, salteando: $entryId"
return
fi
mkdir -p "$destination"
while IFS= read -r sourceFile; do
deployDockerConfigFile "$sourceFile" "$sourceDirectory" "$destination"
done < <(find "$sourceDirectory" -not -type d | sort)
if [[ "$autostart" == "true" ]]; then
local composeFile
if composeFile="$(findDockerComposeFile "$destination")"; then
docker compose -f "$composeFile" up -d \
|| log::warn "No se pudo iniciar el servicio: $entryId"
log::ok "Servicio Docker iniciado: $entryId"
else
log::warn "No se encontró archivo compose en: $destination"
fi
fi
log::ok "Docker configurado: $entryId"
}
# ---- Dispatcher ----
dispatchByType() {
local entryId="$1" entryType="$2"
case "$entryType" in
ppa) applyPpa "$entryId" ;;
apt) applyAptPackage "$entryId" ;;
snap) applySnapPackage "$entryId" ;;
flatpak) applyFlatpakApp "$entryId" ;;
dotfile) applyDotfile "$entryId" ;;
service) applyService "$entryId" ;;
pipewire) applyPipewire "$entryId" ;;
video) applyVideoDrivers "$entryId" ;;
docker) applyDocker "$entryId" ;;
*) log::warn "Tipo no reconocido '$entryType' en entrada: $entryId" ;;
esac
}
processRegistry() {
local totalProcessed=0
while IFS=$'\t' read -r entryId entryType; do
util::isYamlNull "$entryId" && continue
util::isYamlNull "$entryType" && continue
log::info "[$entryType] $entryId"
dispatchByType "$entryId" "$entryType"
((++totalProcessed))
done < <(yq -r '.registry[] | [.id, .type] | @tsv' "$registryConfig")
log::ok "$totalProcessed entrada(s) del registro procesadas"
}
if ! yaml::has "$registryConfig" ".registry[0]"; then
log::info "Registro vacío, salteando"
exit 0
fi
processRegistry