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>
This commit is contained in:
Executable
+414
@@ -0,0 +1,414 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user