bd78fd9fbe
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>
415 lines
12 KiB
Bash
Executable File
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
|