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:
@@ -0,0 +1,205 @@
|
||||
# Agregar entrada al registro STP
|
||||
|
||||
Agregá una nueva entrada al registro STP para: $ARGUMENTS
|
||||
|
||||
---
|
||||
|
||||
## Procedimiento
|
||||
|
||||
1. Leé `config/registry.yaml` para verificar que no exista ya una entrada con el mismo `id`.
|
||||
2. Identificá el tipo correcto según la descripción (ver sección **Tipos** más abajo).
|
||||
3. Insertá la nueva entrada en `config/registry.yaml`, dentro de la lista `registry:`, respetando el orden:
|
||||
- Los `ppa` deben aparecer **antes** de cualquier `apt` que los requiera.
|
||||
- Dentro de cada grupo, el orden es cosmético; seguí el orden existente.
|
||||
4. Confirmá el resultado mostrando la entrada agregada.
|
||||
|
||||
**Archivo a editar:** `config/registry.yaml`
|
||||
|
||||
---
|
||||
|
||||
## Tipos
|
||||
|
||||
### `apt` — paquete del gestor de paquetes
|
||||
|
||||
```yaml
|
||||
- id: nombrePaquete
|
||||
type: apt
|
||||
# package: nombre-real # solo si el nombre real difiere del id
|
||||
# minVersion: "1.2.3" # versión mínima aceptable (opcional)
|
||||
# maxVersion: "1.2.3" # versión máxima aceptable (opcional)
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `package` — opcional; omitir si el id coincide con el nombre real del paquete apt
|
||||
- `minVersion` — opcional; versión mínima aceptable (cadena de versión Debian, ej. `"2.38.1"`)
|
||||
- `maxVersion` — opcional; versión máxima a instalar
|
||||
|
||||
**Comportamiento de instalación según los campos presentes:**
|
||||
|
||||
| `minVersion` | `maxVersion` | Qué instala |
|
||||
|---|---|---|
|
||||
| — | — | última versión disponible |
|
||||
| `1.0` | — | última disponible (best-effort ≥ 1.0) |
|
||||
| — | `2.0` | `package=2.0` |
|
||||
| `1.0` | `2.0` | `package=2.0` (techo del rango aceptable) |
|
||||
| `1.5` | `1.5` | `package=1.5` (versión exacta) |
|
||||
|
||||
Cuando `maxVersion` está presente, el módulo siempre instala esa versión exacta con `--allow-downgrades`, lo que garantiza que la máquina no quede con una versión superior al límite aunque ya tuviera el paquete instalado. Si la versión instalada ya satisface el rango `[minVersion, maxVersion]`, el paso se omite sin reinstalar.
|
||||
|
||||
### `ppa` — repositorio PPA de Ubuntu
|
||||
|
||||
```yaml
|
||||
- id: nombrePpa
|
||||
type: ppa
|
||||
address: ppa:usuario/repositorio
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `address` — requerido, formato `ppa:usuario/repositorio`
|
||||
|
||||
> Debe aparecer en el archivo **antes** del `apt` que instala paquetes de ese PPA.
|
||||
|
||||
### `snap` — aplicación snap
|
||||
|
||||
```yaml
|
||||
- id: nombreApp
|
||||
type: snap
|
||||
# package: nombre-snap # solo si el nombre real difiere del id
|
||||
# classic: true # solo si el snap requiere confinamiento clásico
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `package` — opcional
|
||||
- `classic` — opcional; omitir si es `false`
|
||||
|
||||
### `flatpak` — aplicación Flatpak
|
||||
|
||||
```yaml
|
||||
- id: nombreApp
|
||||
type: flatpak
|
||||
appId: com.ejemplo.App
|
||||
# remote: flathub # opcional; flathub por defecto
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `appId` — requerido, ID completo de la aplicación (ej. `org.gimp.GIMP`)
|
||||
- `remote` — opcional; omitir si es flathub
|
||||
|
||||
### `dotfile` — configuración enlazada a `$HOME`
|
||||
|
||||
```yaml
|
||||
- id: nombreApp
|
||||
type: dotfile
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase; debe coincidir con una carpeta dentro de `dotfiles/`
|
||||
|
||||
Los archivos en `dotfiles/nombreApp/` se enlazan simbólicamente a su ruta equivalente en `$HOME`.
|
||||
Ejemplo: `dotfiles/bash/.bashrc` → `~/.bashrc`
|
||||
|
||||
Si la carpeta `dotfiles/nombreApp/` no existe, crearla y colocar los archivos adentro antes de agregar la entrada.
|
||||
|
||||
### `service` — servicio systemd
|
||||
|
||||
```yaml
|
||||
- id: nombreServicio
|
||||
type: service
|
||||
# name: nombre-servicio # solo si el nombre real difiere del id
|
||||
scope: system # system | user
|
||||
state: enable # enable | disable | mask
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `name` — opcional
|
||||
- `scope` — requerido: `system` o `user`
|
||||
- `state` — requerido: `enable`, `disable` o `mask`
|
||||
|
||||
### `pipewire` — configuración completa de PipeWire
|
||||
|
||||
```yaml
|
||||
- id: pipewire
|
||||
type: pipewire
|
||||
replacePulseaudio: true
|
||||
packages:
|
||||
- pipewire
|
||||
- pipewire-audio
|
||||
- pipewire-pulse
|
||||
- wireplumber
|
||||
- libspa-0.2-bluetooth
|
||||
userServices:
|
||||
- pipewire
|
||||
- pipewire-pulse
|
||||
- wireplumber
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `replacePulseaudio` — `true` para deshabilitar y enmascarar PulseAudio
|
||||
- `packages` — lista de paquetes apt a instalar
|
||||
- `userServices` — lista de servicios systemd de usuario a habilitar
|
||||
|
||||
### `video` — drivers de GPU (detección automática con `lspci`)
|
||||
|
||||
```yaml
|
||||
- id: gpuDrivers
|
||||
type: video
|
||||
drivers:
|
||||
nvidia:
|
||||
packages:
|
||||
- nvidia-driver-535
|
||||
- nvidia-settings
|
||||
amd:
|
||||
packages:
|
||||
- mesa-vulkan-drivers
|
||||
- radeontop
|
||||
- vainfo
|
||||
intel:
|
||||
packages:
|
||||
- intel-media-va-driver
|
||||
- vainfo
|
||||
```
|
||||
|
||||
Solo se instalan los paquetes del vendor detectado. Podés incluir uno, dos o los tres vendors.
|
||||
|
||||
---
|
||||
|
||||
### `docker` — configuración Docker (Compose o Dockerfile)
|
||||
|
||||
```yaml
|
||||
- id: nombreServicio
|
||||
type: docker
|
||||
# destination: ~/docker/nombreServicio # destino en el sistema (opcional, ese es el valor por defecto)
|
||||
# autostart: true # ejecutar docker compose up -d tras el deploy (opcional, false por defecto)
|
||||
```
|
||||
|
||||
Campos:
|
||||
- `id` — requerido, camelCase
|
||||
- `destination` — opcional; directorio de destino donde se enlazan los archivos. Soporta `~`. Por defecto: `~/docker/<id>`
|
||||
- `autostart` — opcional; si `true`, ejecuta `docker compose up -d` después de enlazar los archivos. Requiere un archivo compose en el destino
|
||||
|
||||
Los archivos del servicio deben estar en `docker/<id>/` dentro del repo. Se enlazan simbólicamente a `destination/`, respetando la estructura de subdirectorios.
|
||||
|
||||
Ejemplo de estructura:
|
||||
```
|
||||
docker/
|
||||
miServicio/
|
||||
compose.yaml
|
||||
.env.example
|
||||
config/
|
||||
nginx.conf
|
||||
```
|
||||
|
||||
Si Docker no está instalado en la máquina, el paso se saltea con un aviso sin interrumpir el resto del STP.
|
||||
|
||||
---
|
||||
|
||||
## Convenciones
|
||||
|
||||
- `id` siempre en **camelCase**, sin guiones ni guiones bajos
|
||||
- Omitir campos opcionales cuando no aplican — no usar `null` ni strings vacíos
|
||||
- El campo `name`, `package` o `appId` solo es necesario cuando difiere del `id`
|
||||
@@ -0,0 +1,20 @@
|
||||
Cifrá las claves SSH para almacenarlas en el repo.
|
||||
|
||||
Ejecutá el siguiente comando y seguí las instrucciones en pantalla:
|
||||
|
||||
```bash
|
||||
bash scripts/encryptSsh.sh
|
||||
```
|
||||
|
||||
Si el archivo `secrets/sshKeys.tar.gz.age` se genera correctamente, hacé commit:
|
||||
|
||||
```bash
|
||||
git add secrets/sshKeys.tar.gz.age
|
||||
git commit -m "update encrypted ssh keys"
|
||||
```
|
||||
|
||||
Para verificar que el archivo se puede descifrar antes de hacer commit:
|
||||
|
||||
```bash
|
||||
age --decrypt secrets/sshKeys.tar.gz.age | tar -tzv
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
Creá un nuevo módulo STP llamado `$ARGUMENTS`.
|
||||
|
||||
Seguí estos pasos:
|
||||
|
||||
1. Creá el archivo `modules/$ARGUMENTS.sh` respetando la guía de estilo de `.claude/style.md`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
source "$stpRoot/lib/yaml.sh"
|
||||
|
||||
readonly nombreConfig="$stpRoot/config/$ARGUMENTS.yaml"
|
||||
|
||||
# Funciones auxiliares (camelCase, una responsabilidad cada una)
|
||||
helperEspecifico() { ... }
|
||||
|
||||
# Función principal
|
||||
configurar$ARGUMENTS() {
|
||||
log::info "Configurando $ARGUMENTS..."
|
||||
helperEspecifico
|
||||
log::ok "$ARGUMENTS configurado"
|
||||
}
|
||||
|
||||
# Verificación inicial: salir si no hay configuración
|
||||
if [[ ! -f "$nombreConfig" ]]; then
|
||||
log::info "Sin configuración para $ARGUMENTS, salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
configurar$ARGUMENTS
|
||||
```
|
||||
|
||||
2. Si el módulo necesita configuración propia, creá `config/$ARGUMENTS.yaml`.
|
||||
|
||||
3. Agregá `$ARGUMENTS` en `config/modules` en la posición lógica dentro del flujo:
|
||||
`bootstrap → ssh → registry → <nombre>`
|
||||
|
||||
4. Dale permisos de ejecución: `chmod +x modules/$ARGUMENTS.sh`
|
||||
|
||||
Convenciones obligatorias (ver `.claude/style.md`):
|
||||
- camelCase para todas las variables y funciones, sin guión bajo
|
||||
- Nombres completos, sin abreviaciones (`packageName` no `pkg`)
|
||||
- Funciones booleanas como preguntas (`configuraciónEsVálida()`)
|
||||
- Verificar estado antes de actuar (idempotencia)
|
||||
- Comandos directos — no usar `util::run`
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(bash:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(yq:*)",
|
||||
"Bash(age:*)",
|
||||
"Bash(tar:*)",
|
||||
"Bash(git:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
# Guía de estilo — STP
|
||||
|
||||
## Nomenclatura: camelCase sin excepciones
|
||||
|
||||
Todo identificador del proyecto usa camelCase. No se usa guión bajo (`_`) ni guión (`-`) en nombres de variables, funciones o archivos Bash.
|
||||
|
||||
| Tipo | Correcto | Incorrecto |
|
||||
|------|----------|------------|
|
||||
| Variable local | `packageName` | `package_name`, `pkg` |
|
||||
| Variable exportada | `stpRoot`, `verbose` | `STP_ROOT`, `VERBOSE` |
|
||||
| Constante (`readonly`) | `readonly packagesConfig` | `readonly PACKAGES_CONFIG` |
|
||||
| Función | `installAptPackages()` | `install_apt_packages()` |
|
||||
| Función privada | `log::emit` | `log::_emit`, `_emit` |
|
||||
| Archivo Bash | `encryptSsh.sh` | `encrypt-ssh.sh`, `encrypt_ssh.sh` |
|
||||
|
||||
Los nombres de archivos YAML (`packages.yaml`, `audio.yaml`) son palabras simples y no requieren separador.
|
||||
|
||||
---
|
||||
|
||||
## Clean Code — Robert C. Martin
|
||||
|
||||
### Una función, una responsabilidad
|
||||
|
||||
Cada función hace exactamente una cosa. Un bloque de código que necesita un comentario para explicarse debe convertirse en una función con un nombre descriptivo.
|
||||
|
||||
```bash
|
||||
# MAL — lógica mezclada con comentario explicativo
|
||||
if apt-cache show age &>/dev/null 2>&1; then
|
||||
sudo apt-get install -y age # instala desde apt si está disponible
|
||||
else
|
||||
# descarga binario de GitHub...
|
||||
fi
|
||||
|
||||
# BIEN — la intención es legible sin comentarios
|
||||
if ageIsAvailableInApt; then
|
||||
installAgeFromApt
|
||||
else
|
||||
installAgeFromGithub
|
||||
fi
|
||||
```
|
||||
|
||||
### Nombres completos, sin abreviaciones
|
||||
|
||||
```bash
|
||||
# MAL
|
||||
for pkg in "${packages[@]}"; do ...
|
||||
for svc in "${services[@]}"; do ...
|
||||
|
||||
# BIEN
|
||||
for packageName in "${packages[@]}"; do ...
|
||||
for serviceName in "${services[@]}"; do ...
|
||||
```
|
||||
|
||||
Palabras prohibidas como nombre de variable: `pkg`, `svc`, `src`, `dest`, `dir`, `tmp`, `cfg`, `val`, `ret`, `f`, `i` (salvo índices numéricos en bucles simples).
|
||||
|
||||
### Predicados como preguntas
|
||||
|
||||
Las funciones booleanas se nombran como preguntas que responden verdadero o falso:
|
||||
|
||||
```bash
|
||||
pipewireIsEnabled() # ¿PipeWire está habilitado en la config?
|
||||
isPpaAlreadyAdded() # ¿el PPA ya fue registrado en sources.list?
|
||||
dotfilesDirectoryIsEmpty() # ¿no hay dotfiles para desplegar?
|
||||
```
|
||||
|
||||
### Comentarios solo para el PORQUÉ
|
||||
|
||||
El código expresa el QUÉ. Solo se agrega un comentario cuando el PORQUÉ no es evidente: una restricción externa, una solución provisional, un comportamiento sorpresivo.
|
||||
|
||||
```bash
|
||||
# MAL — el comentario dice lo mismo que el código
|
||||
# Verifica si el PPA ya está en sources.list.d
|
||||
isPpaAlreadyAdded "$ppaAddress" && continue
|
||||
|
||||
# BIEN — el código habla por sí mismo; el comentario solo si hay un "porqué" no obvio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Código funcional sin modo simulación
|
||||
|
||||
No existe `--dry-run`. El sistema es **idempotente**: verifica el estado actual antes de actuar y omite lo que ya está configurado.
|
||||
|
||||
```bash
|
||||
# Antes de instalar un paquete
|
||||
util::isAptInstalled "$packageName" && continue
|
||||
|
||||
# Antes de agregar un PPA
|
||||
isPpaAlreadyAdded "$ppaAddress" && continue
|
||||
|
||||
# Antes de instalar una clave SSH
|
||||
[[ -f "$destination" ]] && { log::warn "Ya existe: $keyFilename"; return 1; }
|
||||
```
|
||||
|
||||
Ejecutar el STP dos veces produce exactamente el mismo resultado que ejecutarlo una vez.
|
||||
|
||||
---
|
||||
|
||||
## Agregar una nueva configuración al sistema
|
||||
|
||||
La forma principal de agregar algo al STP es agregar una entrada a `config/registry.yaml`. No se necesita crear un módulo nuevo para la mayoría de los casos.
|
||||
|
||||
```yaml
|
||||
# Basta con agregar una línea a la lista:
|
||||
- id: neovim
|
||||
type: apt
|
||||
|
||||
# La próxima ejecución de stp.sh la detecta y aplica
|
||||
```
|
||||
|
||||
Tipos disponibles en el registro: `ppa`, `apt`, `snap`, `flatpak`, `dotfile`, `service`, `pipewire`, `video`.
|
||||
|
||||
Solo creá un módulo nuevo cuando la configuración requiera pasos que ningún tipo del registro puede manejar (compilación desde fuente, instaladores externos, configuración interactiva, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Estructura de un módulo
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
source "$stpRoot/lib/yaml.sh" # solo si el módulo lee YAML
|
||||
|
||||
readonly nombreConfig="$stpRoot/config/nombre.yaml"
|
||||
|
||||
# Funciones auxiliares específicas (las más pequeñas y concretas)
|
||||
helperEspecifico() { ... }
|
||||
|
||||
# Funciones principales que usan las auxiliares
|
||||
hacerAlgo() {
|
||||
log::info "Descripción breve..."
|
||||
helperEspecifico
|
||||
log::ok "Completado"
|
||||
}
|
||||
|
||||
# Verificación inicial: salir si no hay configuración
|
||||
if [[ ! -f "$nombreConfig" ]]; then
|
||||
log::info "Sin configuración para este módulo, salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Punto de entrada al final del archivo
|
||||
hacerAlgo
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
/tmp/
|
||||
*.log
|
||||
.env
|
||||
# El directorio secrets/ se commitea (contiene archivos cifrados con age),
|
||||
# pero nunca claves SSH en texto plano
|
||||
secrets/*.tar.gz
|
||||
secrets/*.key
|
||||
@@ -0,0 +1,187 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this project is
|
||||
|
||||
STP (Sistema de Transferencia Personal) is a personal machine-setup automation tool. Running it on a fresh Ubuntu LTS install configures the machine to match the owner's environment: packages, PPAs, SSH keys, dotfiles, audio (PipeWire), video drivers, systemd services, and application configurations. Files (documents, media) are handled separately by Nextcloud and are out of scope.
|
||||
|
||||
## Key commands
|
||||
|
||||
```bash
|
||||
# First-time setup on a new machine
|
||||
bash <(curl -fsSL https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh)
|
||||
|
||||
# Run all modules
|
||||
bash stp.sh
|
||||
|
||||
# Run a single module
|
||||
bash stp.sh --module registry
|
||||
|
||||
# Skip specific modules
|
||||
bash stp.sh --skip ssh
|
||||
|
||||
# List modules in execution order
|
||||
bash stp.sh --list
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Execution flow:** `bootstrap.sh` → clone repo from Gitea → `stp.sh` → modules in the order defined by `config/modules`
|
||||
|
||||
**`stp.sh`** reads module order from `config/modules` (plain text, one name per line, `#` comments — no `yq` required at this level), then runs each `modules/<name>.sh` as a subprocess inheriting the exported `stpRoot` variable.
|
||||
|
||||
**Modules** (`modules/<name>.sh`) source `lib/log.sh`, `lib/utils.sh`, and optionally `lib/yaml.sh`. All modules are idempotent — they check current state before acting.
|
||||
|
||||
Current modules in execution order:
|
||||
- `bootstrap` — installs `yq` and `age` (required by all other modules)
|
||||
- `ssh` — decrypts `secrets/sshKeys.tar.gz.age` with `age` and installs keys into `~/.ssh`
|
||||
- `registry` — reads `config/registry.yaml` and applies every entry (packages, dotfiles, services, drivers, audio)
|
||||
- `thunderbird` — installs Thunderbird (snap) and restores profile from `secrets/thunderbirdProfile.zip`
|
||||
- `claudeCode` — configures the Anthropic apt repository and installs `claude-code`
|
||||
- `easyEffects` — restores EasyEffects config and presets from `secrets/easyEffectsConfig.zip`
|
||||
- `wireplumber` — restores WirePlumber state (default audio device, Bluetooth profiles) from `secrets/wireplumberState.zip`
|
||||
- `cups` — restores CUPS printer definitions and PPD drivers from `secrets/cupsConfig.zip`
|
||||
|
||||
**`modules/registry.sh`** — the main workhorse. Reads all entries from `config/registry.yaml` and dispatches each one to a handler based on its `type`. Supported types: `ppa`, `apt`, `snap`, `flatpak`, `dotfile`, `service`, `pipewire`, `video`, `docker`.
|
||||
|
||||
**`config/registry.yaml`** — the single list of everything to configure on the machine. Adding a new entry is all that's needed to have STP apply it on the next run. Entries are idempotent — each handler checks current system state before acting.
|
||||
|
||||
**`config/settings.yaml`** — machine-independent settings: Gitea host/user/repo coordinates, user identity (`name`, `email`, `username`), and dotfiles `backup` flag.
|
||||
|
||||
**`lib/`** — shared helpers sourced by modules:
|
||||
- `log.sh` — `log::step`, `log::info`, `log::ok`, `log::warn`, `log::error`; internal emit via `log::emit`
|
||||
- `utils.sh` — `util::cmdExists`, `util::isAptInstalled`, `util::isSnapInstalled`, `util::isFlatpakInstalled`, `util::isYamlNull`, `util::confirm`, `util::requireSudo`, `util::keepSudoAlive`, `util::aptUpdateOnce`
|
||||
- `yaml.sh` — thin wrapper around `yq` v4: `yaml::get`, `yaml::getArray`, `yaml::has`
|
||||
|
||||
**`dotfiles/`** — mirrors `$HOME/` structure. Files referenced by `type: dotfile` entries in the registry become symlinks in the real home directory. If a real file already exists at the destination, it is renamed to `${destination}.stpbackup` before the symlink is created.
|
||||
|
||||
**`docker/`** — stores Docker configurations (Compose files, Dockerfiles, etc.) organized by service: `docker/<id>/`. Files are symlinked to the configured `destination` (default `~/docker/<id>`). If `autostart: true`, `docker compose up -d` is run after deploying.
|
||||
|
||||
**`secrets/`** — encrypted and configuration backups, all safe to commit:
|
||||
- `sshKeys.tar.gz.age` — `age --passphrase`-encrypted tar of `~/.ssh`
|
||||
- `thunderbirdProfile.zip` — Thunderbird profile (accounts, filters, extensions; excludes emails and cache)
|
||||
- `easyEffectsConfig.zip` — EasyEffects settings and output presets
|
||||
- `wireplumberState.zip` — WirePlumber state (default audio sink, Bluetooth device profiles, per-app volumes)
|
||||
- `cupsConfig.zip` — CUPS `printers.conf` and PPD driver files for all configured printers
|
||||
|
||||
**`scripts/`** — capture scripts to run on the current machine before pushing:
|
||||
- `encryptSsh.sh` — encrypts `~/.ssh` into `secrets/sshKeys.tar.gz.age`
|
||||
- `thunderbird/capture.sh` — captures Thunderbird Snap profile into `secrets/thunderbirdProfile.zip`
|
||||
- `easyEffects/capture.sh` — captures EasyEffects Flatpak config into `secrets/easyEffectsConfig.zip`
|
||||
- `wireplumber/capture.sh` — captures WirePlumber state into `secrets/wireplumberState.zip`
|
||||
- `cups/capture.sh` — captures CUPS printer config into `secrets/cupsConfig.zip` (requires sudo)
|
||||
|
||||
**`config/keys/`** — GPG public keys for third-party apt repositories:
|
||||
- `claude-code.asc` — Anthropic signing key for the Claude Code apt repository
|
||||
|
||||
**`.claude/`** — Claude Code project configuration:
|
||||
- `settings.json` — allowed bash commands for this project
|
||||
- `style.md` — style guide (camelCase, Clean Code, no dry-run)
|
||||
- `commands/new-module.md` → `/new-module <name>` — scaffolds a new module
|
||||
- `commands/addEntry.md` → `/addEntry <description>` — adds an entry to the registry
|
||||
- `commands/encrypt-ssh.md` → `/encrypt-ssh` — guides SSH key encryption
|
||||
|
||||
## Currently saved configurations
|
||||
|
||||
### Packages (via registry)
|
||||
|
||||
| ID | Type | Package |
|
||||
|---|---|---|
|
||||
| curl, wget, git, vim, htop, tree, unzip | apt | same as ID |
|
||||
| buildEssential | apt | build-essential |
|
||||
| virtualbox | apt | virtualbox |
|
||||
| dbeaverCe | snap | dbeaver-ce |
|
||||
| vscodium | flatpak | com.vscodium.codium |
|
||||
| filezilla | flatpak | org.filezillaproject.Filezilla |
|
||||
| angryIpScanner | flatpak | org.angryip.ipscan |
|
||||
| anydesk | flatpak | com.anydesk.Anydesk |
|
||||
| bitwarden | flatpak | com.bitwarden.desktop |
|
||||
| bottles | flatpak | com.usebottles.bottles |
|
||||
| bruno | flatpak | com.usebruno.Bruno |
|
||||
| nextcloudDesktop | flatpak | com.nextcloud.desktopclient.nextcloud |
|
||||
| easyEffects | flatpak | com.github.wwmm.easyeffects |
|
||||
| flatseal | flatpak | com.github.tchx84.Flatseal |
|
||||
| warehouse | flatpak | io.github.flattool.Warehouse |
|
||||
| freecad | flatpak | org.freecad.FreeCAD |
|
||||
| hidamari | flatpak | io.github.jeffshee.Hidamari |
|
||||
| inkscape | flatpak | org.inkscape.Inkscape |
|
||||
| libreoffice | flatpak | org.libreoffice.LibreOffice |
|
||||
| logseq | flatpak | com.logseq.Logseq |
|
||||
| obsStudio | flatpak | com.obsproject.Studio |
|
||||
| openshot | flatpak | org.openshot.OpenShot |
|
||||
|
||||
### Application configurations (via modules)
|
||||
|
||||
| Module | Secret | What is restored |
|
||||
|---|---|---|
|
||||
| `ssh` | `secrets/sshKeys.tar.gz.age` | SSH keys with correct permissions |
|
||||
| `thunderbird` | `secrets/thunderbirdProfile.zip` | Accounts, filters, extensions (Snap; excludes emails) |
|
||||
| `claudeCode` | `config/keys/claude-code.asc` | Anthropic apt repo + claude-code package |
|
||||
| `easyEffects` | `secrets/easyEffectsConfig.zip` | Settings DB and output presets (HB-Flat, HB-Lite, HB-Mid, Heavy Bass) |
|
||||
| `wireplumber` | `secrets/wireplumberState.zip` | Default audio sink (Bluetooth headphones), A2DP profile, per-app volumes |
|
||||
| `cups` | `secrets/cupsConfig.zip` | 5 printers: Epson L8050 WiFi, Canon MP230, Samsung M2020, POS80, Cups PDF |
|
||||
|
||||
## Adding a new package
|
||||
|
||||
Add an entry to `config/registry.yaml`. The registry handles: `ppa`, `apt`, `snap`, `flatpak`, `dotfile`, `service`, `pipewire`, `video`, `docker`.
|
||||
|
||||
Use `/addEntry <description>` for guided entry creation.
|
||||
|
||||
For complex setups that require steps beyond what the registry types support (GPG key import, profile restoration, custom installers), create a dedicated module with `/new-module <name>`.
|
||||
|
||||
## Adding a new application configuration backup
|
||||
|
||||
1. Create `scripts/<appName>/capture.sh` — reads from the app's data directory and writes a ZIP to `secrets/`
|
||||
2. Create `modules/<appName>.sh` — checks if app is installed, checks if config already exists, restores from ZIP
|
||||
3. Add `<appName>` to `config/modules` after `registry`
|
||||
4. Run `bash scripts/<appName>/capture.sh` on the current machine
|
||||
5. Commit the generated file in `secrets/`
|
||||
|
||||
Restoration is automatic on the next `stp.sh` run. All modules are idempotent — if the config already exists on the target machine, the restore is skipped.
|
||||
|
||||
## Updating a saved configuration
|
||||
|
||||
Re-run the corresponding capture script and commit the updated file:
|
||||
|
||||
```bash
|
||||
bash scripts/thunderbird/capture.sh && git add secrets/thunderbirdProfile.zip
|
||||
bash scripts/easyEffects/capture.sh && git add secrets/easyEffectsConfig.zip
|
||||
bash scripts/wireplumber/capture.sh && git add secrets/wireplumberState.zip
|
||||
bash scripts/cups/capture.sh && git add secrets/cupsConfig.zip
|
||||
bash scripts/encryptSsh.sh && git add secrets/sshKeys.tar.gz.age
|
||||
```
|
||||
|
||||
## Restoring on a new machine
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap (clones repo and runs all modules)
|
||||
bash <(curl -fsSL https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh)
|
||||
```
|
||||
|
||||
The bootstrap runs all modules automatically. Each module skips gracefully if its secret is missing or if the configuration is already in place.
|
||||
|
||||
**After WirePlumber restore**, restart the session manager to apply the Bluetooth device as default:
|
||||
```bash
|
||||
systemctl --user restart wireplumber
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Effect |
|
||||
|---|---|---|
|
||||
| `stpDir` | `~/.stp` | Clone destination used by `bootstrap.sh` |
|
||||
| `stpLogFile` | `/tmp/stp-<timestamp>.log` | Path for the full execution log |
|
||||
|
||||
## Style guide (enforced on all scripts)
|
||||
|
||||
See `.claude/style.md` for the full guide. Key rules:
|
||||
|
||||
- **camelCase everywhere**: variables, functions, file names — no underscores, no hyphens in identifiers
|
||||
- **No abbreviations**: `packageName` not `pkg`, `serviceName` not `svc`, `sourceFile` not `src`; `temporaryDirectory` not `tempDir`, `sourceDirectory` not `sourceDir`
|
||||
- **Small functions with one responsibility**: extract any named block into a function
|
||||
- **Boolean functions named as questions**: `printersAreConfigured()`, `wireplumberStateExists()`
|
||||
- **No `--dry-run`**: the system is idempotent — it checks state before acting
|
||||
- **No `util::run` wrapper**: call commands directly
|
||||
- **Comments only for WHY**, never for WHAT
|
||||
- **Pre-increment `((++n))`** not post-increment `((n++))` — post-increment of 0 exits with code 1 under `set -e`
|
||||
@@ -1,3 +1,261 @@
|
||||
# STP
|
||||
# STP — Sistema de Transferencia Personal
|
||||
|
||||
Sistema de Transferencia Personal
|
||||
Automatiza la configuración completa de una máquina Ubuntu LTS nueva: paquetes, PPAs, dotfiles, claves SSH, audio, drivers de video, servicios systemd y configuraciones de aplicaciones.
|
||||
|
||||
> Los archivos (documentos, fotos, etc.) los maneja Nextcloud por separado. El STP se ocupa del *entorno* del usuario.
|
||||
|
||||
---
|
||||
|
||||
## Primeros pasos en una máquina nueva
|
||||
|
||||
**Opción A — ejecución directa:**
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh)
|
||||
```
|
||||
|
||||
**Opción B — manual:**
|
||||
|
||||
```bash
|
||||
curl -fsSLo bootstrap.sh https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh
|
||||
bash bootstrap.sh
|
||||
```
|
||||
|
||||
El `bootstrap.sh` instala `git`, `curl` y otras dependencias base si no están disponibles, clona el repo en `~/.stp` (configurable con `stpDir`), y lanza `stp.sh`.
|
||||
|
||||
Durante la ejecución se pedirá la contraseña de las claves SSH cifradas por consola (módulo `ssh`) y permisos de administrador para restaurar impresoras (módulo `cups`).
|
||||
|
||||
**Después de que termine**, reiniciá el gestor de audio para que los auriculares Bluetooth queden como dispositivo por defecto:
|
||||
|
||||
```bash
|
||||
systemctl --user restart wireplumber
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comandos principales
|
||||
|
||||
```bash
|
||||
# Ejecutar todos los módulos en orden
|
||||
bash stp.sh
|
||||
|
||||
# Ejecutar un solo módulo
|
||||
bash stp.sh --module registry
|
||||
|
||||
# Saltear módulos específicos
|
||||
bash stp.sh --skip ssh
|
||||
|
||||
# Ver módulos disponibles (en orden de ejecución)
|
||||
bash stp.sh --list
|
||||
|
||||
# Salida detallada (muestra también los ítems ya instalados)
|
||||
bash stp.sh --verbose
|
||||
```
|
||||
|
||||
El STP es **idempotente**: verifica el estado antes de actuar. Ejecutarlo varias veces es seguro.
|
||||
|
||||
---
|
||||
|
||||
## Orden de ejecución de módulos
|
||||
|
||||
El orden está definido en `config/modules` (un módulo por línea, `#` para comentarios).
|
||||
|
||||
| Módulo | Qué hace |
|
||||
|---|---|
|
||||
| `bootstrap` | Instala `yq` y `age` (requeridos por los demás módulos) |
|
||||
| `ssh` | Descifra e instala claves SSH en `~/.ssh` |
|
||||
| `registry` | Aplica todo lo declarado en `config/registry.yaml` |
|
||||
| `thunderbird` | Instala Thunderbird (snap) y restaura el perfil |
|
||||
| `claudeCode` | Configura el repositorio apt de Anthropic e instala `claude-code` |
|
||||
| `easyEffects` | Restaura configuración y presets de EasyEffects |
|
||||
| `wireplumber` | Restaura el estado de audio: dispositivo Bluetooth por defecto, perfiles, volúmenes por app |
|
||||
| `cups` | Restaura las impresoras configuradas y sus drivers PPD |
|
||||
|
||||
---
|
||||
|
||||
## Configuraciones actualmente guardadas
|
||||
|
||||
### Paquetes (via registro)
|
||||
|
||||
| ID | Tipo | Paquete |
|
||||
|---|---|---|
|
||||
| curl, wget, git, vim, htop, tree, unzip | apt | igual al ID |
|
||||
| buildEssential | apt | build-essential |
|
||||
| virtualbox | apt | virtualbox |
|
||||
| dbeaverCe | snap | dbeaver-ce |
|
||||
| vscodium | flatpak | com.vscodium.codium |
|
||||
| filezilla | flatpak | org.filezillaproject.Filezilla |
|
||||
| angryIpScanner | flatpak | org.angryip.ipscan |
|
||||
| anydesk | flatpak | com.anydesk.Anydesk |
|
||||
| bitwarden | flatpak | com.bitwarden.desktop |
|
||||
| bottles | flatpak | com.usebottles.bottles |
|
||||
| bruno | flatpak | com.usebruno.Bruno |
|
||||
| nextcloudDesktop | flatpak | com.nextcloud.desktopclient.nextcloud |
|
||||
| easyEffects | flatpak | com.github.wwmm.easyeffects |
|
||||
| flatseal | flatpak | com.github.tchx84.Flatseal |
|
||||
| warehouse | flatpak | io.github.flattool.Warehouse |
|
||||
| freecad | flatpak | org.freecad.FreeCAD |
|
||||
| hidamari | flatpak | io.github.jeffshee.Hidamari |
|
||||
| inkscape | flatpak | org.inkscape.Inkscape |
|
||||
| libreoffice | flatpak | org.libreoffice.LibreOffice |
|
||||
| logseq | flatpak | com.logseq.Logseq |
|
||||
| obsStudio | flatpak | com.obsproject.Studio |
|
||||
| openshot | flatpak | org.openshot.OpenShot |
|
||||
|
||||
### Configuraciones de aplicaciones (via módulos)
|
||||
|
||||
| Módulo | Archivo | Qué se restaura |
|
||||
|---|---|---|
|
||||
| `ssh` | `secrets/sshKeys.tar.gz.age` | Claves SSH con permisos correctos |
|
||||
| `thunderbird` | `secrets/thunderbirdProfile.zip` | Cuentas, filtros y extensiones (excluye emails) |
|
||||
| `claudeCode` | `config/keys/claude-code.asc` | Repositorio apt de Anthropic + paquete claude-code |
|
||||
| `easyEffects` | `secrets/easyEffectsConfig.zip` | Configuración y presets de salida (HB-Flat, HB-Lite, HB-Mid, Heavy Bass) |
|
||||
| `wireplumber` | `secrets/wireplumberState.zip` | Auriculares Bluetooth como salida por defecto, perfil A2DP, volúmenes por app |
|
||||
| `cups` | `secrets/cupsConfig.zip` | 5 impresoras: Epson L8050 WiFi, Canon MP230, Samsung M2020, POS80, Cups PDF |
|
||||
|
||||
---
|
||||
|
||||
## Actualizar una configuración guardada
|
||||
|
||||
Volvé a ejecutar el script de captura correspondiente y commiteá el archivo generado:
|
||||
|
||||
```bash
|
||||
bash scripts/encryptSsh.sh && git add secrets/sshKeys.tar.gz.age
|
||||
bash scripts/thunderbird/capture.sh && git add secrets/thunderbirdProfile.zip
|
||||
bash scripts/easyEffects/capture.sh && git add secrets/easyEffectsConfig.zip
|
||||
bash scripts/wireplumber/capture.sh && git add secrets/wireplumberState.zip
|
||||
bash scripts/cups/capture.sh && git add secrets/cupsConfig.zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agregar un paquete nuevo
|
||||
|
||||
Basta con agregar una entrada a `config/registry.yaml`. La próxima vez que corra el STP, detectará la nueva entrada y la aplicará.
|
||||
|
||||
```bash
|
||||
# Desde Claude Code — agrega la entrada correctamente formateada
|
||||
/addEntry neovim apt
|
||||
/addEntry gimp flatpak appId=org.gimp.GIMP
|
||||
/addEntry bashrc dotfile
|
||||
```
|
||||
|
||||
O editá directamente `config/registry.yaml`:
|
||||
|
||||
```yaml
|
||||
registry:
|
||||
- id: neovim
|
||||
type: apt
|
||||
|
||||
- id: vscode
|
||||
type: snap
|
||||
package: code
|
||||
classic: true
|
||||
|
||||
- id: gimp
|
||||
type: flatpak
|
||||
appId: org.gimp.GIMP
|
||||
|
||||
- id: bash
|
||||
type: dotfile
|
||||
|
||||
- id: networkManager
|
||||
type: service
|
||||
scope: system
|
||||
state: enable
|
||||
```
|
||||
|
||||
### Tipos disponibles en el registro
|
||||
|
||||
| Tipo | Qué hace |
|
||||
|---|---|
|
||||
| `ppa` | Agrega un repositorio PPA a apt |
|
||||
| `apt` | Instala un paquete con apt (soporta `minVersion` / `maxVersion`) |
|
||||
| `snap` | Instala un paquete snap |
|
||||
| `flatpak` | Instala una aplicación Flatpak |
|
||||
| `dotfile` | Enlaza todos los archivos de `dotfiles/<id>/` a sus rutas en `$HOME/` |
|
||||
| `service` | Habilita, deshabilita o mask un servicio systemd |
|
||||
| `pipewire` | Instala y configura PipeWire como sistema de audio |
|
||||
| `video` | Detecta la GPU y instala los drivers correspondientes |
|
||||
| `docker` | Despliega archivos de configuración Docker desde `docker/<id>/` |
|
||||
|
||||
---
|
||||
|
||||
## Agregar una nueva configuración de aplicación
|
||||
|
||||
Para guardar y restaurar la configuración de una app nueva:
|
||||
|
||||
1. Crear `scripts/<appName>/capture.sh` — lee desde el directorio de datos de la app y genera un ZIP en `secrets/`
|
||||
2. Crear `modules/<appName>.sh` — verifica si la app está instalada y si ya hay config; si no, restaura desde el ZIP
|
||||
3. Agregar `<appName>` en `config/modules` después de `registry`
|
||||
4. Ejecutar `bash scripts/<appName>/capture.sh` en la máquina actual
|
||||
5. Commitear el archivo generado en `secrets/`
|
||||
|
||||
La restauración es automática en el próximo `stp.sh`. Si la config ya existe en la máquina destino, el módulo la saltea.
|
||||
|
||||
```bash
|
||||
# Desde Claude Code — genera la estructura completa
|
||||
/new-module nombreApp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dotfiles
|
||||
|
||||
Cada aplicación tiene su propia carpeta dentro de `dotfiles/`. Dentro de esa carpeta, los archivos respetan la misma ruta que tendrían en `$HOME/`:
|
||||
|
||||
```
|
||||
dotfiles/
|
||||
├── bash/
|
||||
│ └── .bashrc
|
||||
├── git/
|
||||
│ ├── .gitconfig
|
||||
│ └── .gitignore_global
|
||||
└── neovim/
|
||||
└── .config/
|
||||
└── nvim/
|
||||
└── init.lua
|
||||
```
|
||||
|
||||
Para activar una carpeta, basta con agregar una entrada en el registro:
|
||||
|
||||
```yaml
|
||||
- id: bash
|
||||
type: dotfile
|
||||
|
||||
- id: neovim
|
||||
type: dotfile
|
||||
```
|
||||
|
||||
El STP recorre todos los archivos de `dotfiles/bash/` y crea un enlace simbólico por cada uno en su ruta equivalente dentro de `$HOME/`. Si un archivo ya existe, guarda una copia con extensión `.stpbackup` antes de reemplazarlo.
|
||||
|
||||
---
|
||||
|
||||
## Claves SSH
|
||||
|
||||
Las claves se almacenan cifradas en el repo como `secrets/sshKeys.tar.gz.age`. El archivo está cifrado con `age` usando una contraseña y **es seguro hacer commit de él**.
|
||||
|
||||
**Cifrar las claves actuales** (correr en la máquina con las claves):
|
||||
|
||||
```bash
|
||||
bash scripts/encryptSsh.sh
|
||||
# → genera secrets/sshKeys.tar.gz.age
|
||||
git add secrets/sshKeys.tar.gz.age && git commit -m "update ssh keys"
|
||||
```
|
||||
|
||||
**Verificar el cifrado antes de hacer commit:**
|
||||
|
||||
```bash
|
||||
age --decrypt secrets/sshKeys.tar.gz.age | tar -tzv
|
||||
```
|
||||
|
||||
Durante la ejecución del STP, el módulo `ssh` pide la contraseña, descifra el archivo e instala las claves en `~/.ssh` con los permisos correctos. Las claves que ya existen se saltean sin sobrescribir.
|
||||
|
||||
---
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Predeterminado | Efecto |
|
||||
|---|---|---|
|
||||
| `stpDir` | `~/.stp` | Directorio de clonado (bootstrap) |
|
||||
| `stpLogFile` | `/tmp/stp-<timestamp>.log` | Ruta del log completo |
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Corré esto en una máquina nueva. Clona el repo STP y lanza stp.sh.
|
||||
#
|
||||
# Uso:
|
||||
# bash <(curl -fsSL https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh)
|
||||
#
|
||||
# O descargá y ejecutá manualmente:
|
||||
# curl -fsSLo bootstrap.sh https://gitea.mateosaldain.uy/mateo/stp/raw/branch/main/bootstrap.sh
|
||||
# bash bootstrap.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly giteaHost="gitea.mateosaldain.uy"
|
||||
readonly giteaUser="mateo"
|
||||
readonly repoName="stp"
|
||||
readonly cloneUrl="https://${giteaHost}/${giteaUser}/${repoName}.git"
|
||||
cloneDir="${stpDir:-$HOME/.stp}"
|
||||
|
||||
echo "=== Sistema de Transferencia Personal ==="
|
||||
echo ""
|
||||
|
||||
if ! command -v git &>/dev/null || ! command -v curl &>/dev/null; then
|
||||
echo "-> Instalando git y curl..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y git curl
|
||||
fi
|
||||
|
||||
if [[ -d "$cloneDir/.git" ]]; then
|
||||
echo "-> Actualizando repositorio STP en $cloneDir..."
|
||||
git -C "$cloneDir" pull --ff-only
|
||||
else
|
||||
echo "-> Clonando repositorio STP en $cloneDir..."
|
||||
git clone "$cloneUrl" "$cloneDir"
|
||||
fi
|
||||
|
||||
chmod +x "$cloneDir/stp.sh"
|
||||
|
||||
echo ""
|
||||
echo "-> Lanzando STP..."
|
||||
exec bash "$cloneDir/stp.sh" "$@"
|
||||
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGnK73ABEACnbytJXkjweYrwIr0aLEFRlH+C0nF44KxFc7gQmJ6PjSPMGZAD
|
||||
dxZcaixU7zZl8WxEpVO0wLmIH8cf2zGOdyuZg1Yaugk1vHb2b8WBhAGCQJdPgB8W
|
||||
XquedepEYtk56uP/gCoTjJDUZluEGBHnlnuujSJ4orxEdhSykEoAUfJZGEILPpMd
|
||||
bphFt/Sn+Eb/TxM5jpKPdwnv8AShNF/1mZU1fWTQq9tRKJUakZj04gdaDFElQXak
|
||||
CtTij+GT6yoYCARSHwGO+PC/Pr6q4tc+D7LRjxSBvUWDoFSmlqb/PJ1hj9D/7I2O
|
||||
e4XXniAPWMR56KvxHlzOzrNQdJujbJdSkCwh1ZijkSd3y8ayW5WYUTGdRab99NUw
|
||||
agzlabe/VVF6kzJ0Scn5q3PihB2Y9Bwo0CKnkYk7a7KT77EWv0Kkq+VHmOtqX3a2
|
||||
hhX+b6a6ve9rzJ1qZYGj+obv/C3Sx1LzUjAfqVy7RJDf2uAoP5t2g8u/TkSpUxhM
|
||||
VEjZBkSxYZhMyzQM6t8IgkUfnSrIPTHixbDWARZ4beMOBjxyPZK1nP7OOrNR3TkK
|
||||
JtwLMQAabURCDnL0PjS0iwBTU4jtumBD1XSULyWuoTvMljrpQr1nV1oDyOt0OLqa
|
||||
KA2McWtd9PdXhC8y2EIg7TmrTlJLfHYbdmkiCYj4J49Q8HWkN/6WE+RTUwARAQAB
|
||||
tD5BbnRocm9waWMgQ2xhdWRlIENvZGUgUmVsZWFzZSBTaWduaW5nIDxzZWN1cml0
|
||||
eUBhbnRocm9waWMuY29tPokCUQQTAQoAOxYhBDHd3iTd+rZ59C170rqpKf8afsrO
|
||||
BQJpyu9wAhsPBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELqpKf8afsrO
|
||||
l5IP/2I8X1dFy5xYczWB/coIxGjuzS/V6ByZGZZEJsbr04pmuHiFUykJqPGWGQ6q
|
||||
U0YF5iEwvEkaagS5m7DzhSEf3FM3Cgafax/6d70tar9Vr1D+w6uPfxetu7u/WYJp
|
||||
aolIsdh5fTrBh9zSM1Njl8FM8wG8CwZQjS33Oa7d8cwRkgdUWbt6LXgz+cTQNuBn
|
||||
BgW6Ks7oZFI25dfu0ojDR+aDFJg4+4wZoyDLPvJz1SIrJ5WFGs67zsx9SfS3yZnf
|
||||
XKmBe+f0dUy+GJ2nFZrXFf99+c0dPEHYO8DCeAHZizjkFrdYtUHdDU0YDYEGkLJa
|
||||
bE+pgcpkHf5EvsZzHsyDbl95W/eh7pcXMbwkN+W4CBYUE9X4uHhqzWaC5yAVRWUA
|
||||
1BJ9V4LjZfHPLEJt0I3TxzXiEg9/BVeaTYq9RjaxIFo9Nfk158HqJY6SA5jslBlx
|
||||
Gv/No8u+xVcze2UJyGVfEIUfm92+0UAIkny3+5cuVV0ICzJxXlXj0CnLM9Lt50wE
|
||||
p3suVwuBEviCbZ08eAH1Ht8gbBdSsiOkIU8CX3v/scwHHx5q0+NBL6xLrQObg13a
|
||||
tRXBlKObfElkPN3lTUbUnJOW4U8uSjH8VRP+AujKWMDFe7x0zCs+iYY1mTOvbrTS
|
||||
9n3CmZUmbynZ+E/QWNENpW/pDNZdWFy43PASmML5FHu4m9Sn
|
||||
=oqMI
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,12 @@
|
||||
# Orden de ejecución de los módulos STP.
|
||||
# Una entrada por línea. Líneas con # son comentarios.
|
||||
# El nombre corresponde a modules/<nombre>.sh
|
||||
|
||||
bootstrap
|
||||
ssh
|
||||
registry
|
||||
thunderbird
|
||||
claudeCode
|
||||
easyEffects
|
||||
wireplumber
|
||||
cups
|
||||
@@ -0,0 +1,180 @@
|
||||
# Registro unificado de configuraciones STP
|
||||
#
|
||||
# Para agregar algo nuevo: basta con agregar una entrada a esta lista.
|
||||
# El próximo run del STP detectará la entrada y la aplicará.
|
||||
# Las entradas son idempotentes — ejecutar el STP múltiples veces es seguro.
|
||||
#
|
||||
# Tipos disponibles:
|
||||
# ppa — repositorio PPA de Ubuntu
|
||||
# apt — paquete del gestor apt
|
||||
# snap — paquete snap
|
||||
# flatpak — aplicación Flatpak
|
||||
# dotfile — archivo de configuración (symlink en $HOME)
|
||||
# service — servicio systemd (enable / disable / mask)
|
||||
# pipewire — configuración completa de PipeWire
|
||||
# video — drivers de video (detección automática de GPU)
|
||||
#
|
||||
# Orden importa: los PPAs deben aparecer antes de los paquetes que los requieren.
|
||||
|
||||
registry:
|
||||
|
||||
# PPAs
|
||||
# - id: gitPpa
|
||||
# type: ppa
|
||||
# address: ppa:git-core/ppa
|
||||
|
||||
# Paquetes apt
|
||||
# El campo "package" es opcional si el id coincide con el nombre real del paquete.
|
||||
- id: curl
|
||||
type: apt
|
||||
|
||||
- id: wget
|
||||
type: apt
|
||||
|
||||
- id: git
|
||||
type: apt
|
||||
|
||||
- id: vim
|
||||
type: apt
|
||||
|
||||
- id: htop
|
||||
type: apt
|
||||
|
||||
- id: tree
|
||||
type: apt
|
||||
|
||||
- id: unzip
|
||||
type: apt
|
||||
|
||||
- id: buildEssential
|
||||
type: apt
|
||||
package: build-essential
|
||||
|
||||
- id: virtualbox
|
||||
type: apt
|
||||
|
||||
# Snaps
|
||||
- id: dbeaverCe
|
||||
type: snap
|
||||
package: dbeaver-ce
|
||||
|
||||
# Flatpaks
|
||||
- id: vscodium
|
||||
type: flatpak
|
||||
appId: com.vscodium.codium
|
||||
|
||||
- id: filezilla
|
||||
type: flatpak
|
||||
appId: org.filezillaproject.Filezilla
|
||||
|
||||
- id: angryIpScanner
|
||||
type: flatpak
|
||||
appId: org.angryip.ipscan
|
||||
|
||||
- id: anydesk
|
||||
type: flatpak
|
||||
appId: com.anydesk.Anydesk
|
||||
|
||||
- id: bitwarden
|
||||
type: flatpak
|
||||
appId: com.bitwarden.desktop
|
||||
|
||||
- id: bottles
|
||||
type: flatpak
|
||||
appId: com.usebottles.bottles
|
||||
|
||||
- id: bruno
|
||||
type: flatpak
|
||||
appId: com.usebruno.Bruno
|
||||
|
||||
- id: nextcloudDesktop
|
||||
type: flatpak
|
||||
appId: com.nextcloud.desktopclient.nextcloud
|
||||
|
||||
- id: easyEffects
|
||||
type: flatpak
|
||||
appId: com.github.wwmm.easyeffects
|
||||
|
||||
- id: flatseal
|
||||
type: flatpak
|
||||
appId: com.github.tchx84.Flatseal
|
||||
|
||||
- id: warehouse
|
||||
type: flatpak
|
||||
appId: io.github.flattool.Warehouse
|
||||
|
||||
- id: freecad
|
||||
type: flatpak
|
||||
appId: org.freecad.FreeCAD
|
||||
|
||||
- id: hidamari
|
||||
type: flatpak
|
||||
appId: io.github.jeffshee.Hidamari
|
||||
|
||||
- id: inkscape
|
||||
type: flatpak
|
||||
appId: org.inkscape.Inkscape
|
||||
|
||||
- id: libreoffice
|
||||
type: flatpak
|
||||
appId: org.libreoffice.LibreOffice
|
||||
|
||||
- id: logseq
|
||||
type: flatpak
|
||||
appId: com.logseq.Logseq
|
||||
|
||||
- id: obsStudio
|
||||
type: flatpak
|
||||
appId: com.obsproject.Studio
|
||||
|
||||
- id: openshot
|
||||
type: flatpak
|
||||
appId: org.openshot.OpenShot
|
||||
|
||||
# VMware Workstation/Player requiere el instalador .bundle descargado desde vmware.com.
|
||||
# No puede instalarse vía registro. Creá un módulo dedicado con: /new-module vmware
|
||||
|
||||
# Dotfiles
|
||||
# Cada entrada apunta a una carpeta dentro de dotfiles/<id>/.
|
||||
# Los archivos dentro se enlazan automáticamente a su ruta equivalente en $HOME/.
|
||||
# Ejemplo: dotfiles/bash/.bashrc → ~/.bashrc
|
||||
|
||||
# Servicios systemd
|
||||
# - id: networkManager
|
||||
# type: service
|
||||
# name: NetworkManager # nombre real del servicio (si difiere del id)
|
||||
# scope: system # system o user
|
||||
# state: enable # enable, disable o mask
|
||||
|
||||
# PipeWire
|
||||
# - id: pipewire
|
||||
# type: pipewire
|
||||
# replacePulseaudio: true
|
||||
# packages:
|
||||
# - pipewire
|
||||
# - pipewire-audio
|
||||
# - pipewire-pulse
|
||||
# - wireplumber
|
||||
# - libspa-0.2-bluetooth
|
||||
# userServices:
|
||||
# - pipewire
|
||||
# - pipewire-pulse
|
||||
# - wireplumber
|
||||
|
||||
# Video (detección automática de GPU con lspci)
|
||||
# - id: gpuDrivers
|
||||
# type: video
|
||||
# drivers:
|
||||
# nvidia:
|
||||
# packages:
|
||||
# - nvidia-driver-535
|
||||
# - nvidia-settings
|
||||
# amd:
|
||||
# packages:
|
||||
# - mesa-vulkan-drivers
|
||||
# - radeontop
|
||||
# - vainfo
|
||||
# intel:
|
||||
# packages:
|
||||
# - intel-media-va-driver
|
||||
# - vainfo
|
||||
@@ -0,0 +1,14 @@
|
||||
gitea:
|
||||
host: gitea.mateosaldain.uy
|
||||
user: mateo
|
||||
repo: stp
|
||||
|
||||
user:
|
||||
name: Mateo Saldain
|
||||
email: saldainmateo@gmail.com
|
||||
username: mateo
|
||||
|
||||
dotfiles:
|
||||
# Crea symlinks de dotfiles/ hacia $HOME. Con backup:true guarda los archivos
|
||||
# existentes en ~/.stp-backup/<timestamp>/ antes de reemplazarlos.
|
||||
backup: true
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
readonly clrRed='\033[0;31m'
|
||||
readonly clrGreen='\033[0;32m'
|
||||
readonly clrYellow='\033[1;33m'
|
||||
readonly clrBlue='\033[0;34m'
|
||||
readonly clrCyan='\033[0;36m'
|
||||
readonly clrBold='\033[1m'
|
||||
readonly clrReset='\033[0m'
|
||||
|
||||
stpLogFile="${stpLogFile:-/tmp/stp-$(date +%Y%m%d-%H%M%S).log}"
|
||||
export stpLogFile
|
||||
|
||||
log::emit() {
|
||||
local level="$1" color="$2" symbol="$3"
|
||||
shift 3
|
||||
local message="$*"
|
||||
local timestamp
|
||||
timestamp="$(date '+%H:%M:%S')"
|
||||
printf "${color}${symbol}${clrReset} %s\n" "$message"
|
||||
printf "[%s] [%-5s] %s\n" "$timestamp" "$level" "$message" >> "$stpLogFile"
|
||||
}
|
||||
|
||||
log::step() { log::emit "STEP" "${clrBold}${clrBlue}" "==>" "$@"; }
|
||||
log::info() { log::emit "INFO" "${clrCyan}" " -> " "$@"; }
|
||||
log::ok() { log::emit "OK" "${clrGreen}" " ✓ " "$@"; }
|
||||
log::warn() { log::emit "WARN" "${clrYellow}" " ! " "$@"; }
|
||||
log::error() { log::emit "ERROR" "${clrRed}" " ✗ " "$@"; }
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
util::cmdExists() { command -v "$1" &>/dev/null; }
|
||||
util::isAptInstalled() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; }
|
||||
util::isSnapInstalled() { snap list "$1" &>/dev/null 2>&1; }
|
||||
util::isFlatpakInstalled() { flatpak list --app --columns=application 2>/dev/null | grep -q "^$1$"; }
|
||||
util::isYamlNull() { [[ -z "$1" || "$1" == "null" ]]; }
|
||||
|
||||
util::confirm() {
|
||||
local question="${1:-¿Continuar?} [s/N] "
|
||||
local answer
|
||||
read -rp "$question" answer
|
||||
[[ "${answer,,}" == "s" || "${answer,,}" == "si" || "${answer,,}" == "sí" ]]
|
||||
}
|
||||
|
||||
util::keepSudoAlive() {
|
||||
while true; do
|
||||
sudo -n true
|
||||
sleep 50
|
||||
kill -0 "$$" || exit
|
||||
done 2>/dev/null &
|
||||
}
|
||||
|
||||
util::requireSudo() {
|
||||
[[ "$EUID" -eq 0 ]] && return
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
log::info "Se requieren permisos de administrador"
|
||||
sudo -v
|
||||
fi
|
||||
util::keepSudoAlive
|
||||
}
|
||||
|
||||
util::aptUpdateOnce() {
|
||||
if [[ "${aptUpdated:-false}" != "true" ]]; then
|
||||
log::info "Actualizando índices de apt..."
|
||||
sudo apt-get update -qq
|
||||
aptUpdated=true
|
||||
export aptUpdated
|
||||
fi
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
yaml::get() {
|
||||
local file="$1" query="$2"
|
||||
yq "$query" "$file"
|
||||
}
|
||||
|
||||
yaml::getArray() {
|
||||
local file="$1" query="$2"
|
||||
yq -r "${query} | .[]" "$file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
yaml::has() {
|
||||
local file="$1" query="$2"
|
||||
local value
|
||||
value="$(yq "$query" "$file" 2>/dev/null)"
|
||||
[[ -n "$value" && "$value" != "null" ]]
|
||||
}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
latestGithubReleaseTag() {
|
||||
local repository="$1"
|
||||
curl -fsSL "https://api.github.com/repos/${repository}/releases/latest" \
|
||||
| grep '"tag_name"' \
|
||||
| cut -d'"' -f4
|
||||
}
|
||||
|
||||
ensureBasePackagesAreInstalled() {
|
||||
local requiredPackages=(curl wget git nano gpg ca-certificates)
|
||||
local missingPackages=()
|
||||
|
||||
for packageName in "${requiredPackages[@]}"; do
|
||||
util::isAptInstalled "$packageName" || missingPackages+=("$packageName")
|
||||
done
|
||||
|
||||
if [[ ${#missingPackages[@]} -gt 0 ]]; then
|
||||
util::aptUpdateOnce
|
||||
sudo apt-get install -y "${missingPackages[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
installYqFromGithub() {
|
||||
local version
|
||||
version="$(latestGithubReleaseTag "mikefarah/yq")"
|
||||
sudo wget -qO /usr/local/bin/yq \
|
||||
"https://github.com/mikefarah/yq/releases/download/${version}/yq_linux_amd64"
|
||||
sudo chmod +x /usr/local/bin/yq
|
||||
log::ok "yq ${version} instalado"
|
||||
}
|
||||
|
||||
ensureYqIsInstalled() {
|
||||
if util::cmdExists yq; then
|
||||
log::info "yq ya disponible: $(yq --version 2>&1 | head -1)"
|
||||
return
|
||||
fi
|
||||
log::info "Instalando yq..."
|
||||
installYqFromGithub
|
||||
}
|
||||
|
||||
ageIsAvailableInApt() {
|
||||
apt-cache show age &>/dev/null 2>&1
|
||||
}
|
||||
|
||||
installAgeFromApt() {
|
||||
sudo apt-get install -y age
|
||||
}
|
||||
|
||||
installAgeFromGithub() {
|
||||
local version
|
||||
version="$(latestGithubReleaseTag "FiloSottile/age")"
|
||||
local temporaryDirectory
|
||||
temporaryDirectory="$(mktemp -d)"
|
||||
trap 'rm -rf "$temporaryDirectory"' RETURN
|
||||
curl -fsSL \
|
||||
"https://github.com/FiloSottile/age/releases/download/${version}/age-${version}-linux-amd64.tar.gz" \
|
||||
| tar -xz -C "$temporaryDirectory"
|
||||
sudo mv "$temporaryDirectory/age/age" "$temporaryDirectory/age/age-keygen" /usr/local/bin/
|
||||
}
|
||||
|
||||
ensureAgeIsInstalled() {
|
||||
if util::cmdExists age; then
|
||||
log::info "age ya disponible: $(age --version 2>&1)"
|
||||
return
|
||||
fi
|
||||
log::info "Instalando age..."
|
||||
if ageIsAvailableInApt; then
|
||||
installAgeFromApt
|
||||
else
|
||||
installAgeFromGithub
|
||||
fi
|
||||
log::ok "age instalado"
|
||||
}
|
||||
|
||||
log::info "Verificando dependencias base"
|
||||
ensureBasePackagesAreInstalled
|
||||
ensureYqIsInstalled
|
||||
ensureAgeIsInstalled
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly claudeCodeKeyring="/etc/apt/keyrings/claude-code.asc"
|
||||
readonly claudeCodeSources="/etc/apt/sources.list.d/claude-code.list"
|
||||
readonly claudeCodeSourcesEntry="deb [signed-by=${claudeCodeKeyring}] https://downloads.claude.ai/claude-code/apt/stable stable main"
|
||||
|
||||
repoIsConfigured() {
|
||||
[[ -f "$claudeCodeSources" ]]
|
||||
}
|
||||
|
||||
addSigningKey() {
|
||||
sudo install -m 644 "$stpRoot/config/keys/claude-code.asc" "$claudeCodeKeyring"
|
||||
log::ok "Clave GPG instalada: $claudeCodeKeyring"
|
||||
}
|
||||
|
||||
addRepository() {
|
||||
echo "$claudeCodeSourcesEntry" | sudo tee "$claudeCodeSources" > /dev/null
|
||||
sudo apt-get update -qq
|
||||
log::ok "Repositorio configurado: $claudeCodeSources"
|
||||
}
|
||||
|
||||
if repoIsConfigured; then
|
||||
log::info "Repositorio claude-code ya configurado"
|
||||
else
|
||||
addSigningKey
|
||||
addRepository
|
||||
fi
|
||||
|
||||
if util::isAptInstalled "claude-code"; then
|
||||
log::info "Ya instalado (apt): claude-code"
|
||||
else
|
||||
sudo apt-get install -y claude-code
|
||||
log::ok "Instalado: claude-code"
|
||||
fi
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly cupsArchive="$stpRoot/secrets/cupsConfig.zip"
|
||||
|
||||
cupsIsInstalled() {
|
||||
util::cmdExists lpstat
|
||||
}
|
||||
|
||||
printersAreConfigured() {
|
||||
sudo test -s /etc/cups/printers.conf
|
||||
}
|
||||
|
||||
restoreConfig() {
|
||||
log::info "Restaurando configuración de CUPS desde secrets/cupsConfig.zip..."
|
||||
unzip -q "$cupsArchive" -d "$cupsTemp"
|
||||
|
||||
sudo cp "$cupsTemp/printers.conf" /etc/cups/printers.conf
|
||||
sudo chown root:lp /etc/cups/printers.conf
|
||||
sudo chmod 600 /etc/cups/printers.conf
|
||||
|
||||
if [[ -d "$cupsTemp/ppd" ]]; then
|
||||
sudo mkdir -p /etc/cups/ppd
|
||||
sudo cp -r "$cupsTemp/ppd/." /etc/cups/ppd/
|
||||
sudo chown -R root:lp /etc/cups/ppd/
|
||||
sudo chmod 644 /etc/cups/ppd/*.ppd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
sudo systemctl restart cups
|
||||
log::ok "Configuración de CUPS restaurada"
|
||||
}
|
||||
|
||||
if ! cupsIsInstalled; then
|
||||
log::info "CUPS no instalado, salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$cupsArchive" ]]; then
|
||||
log::info "Sin respaldo de CUPS (secrets/cupsConfig.zip), salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
util::requireSudo
|
||||
|
||||
if printersAreConfigured; then
|
||||
log::info "CUPS ya tiene impresoras configuradas, salteando restauración"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cupsTemp="$(mktemp -d)"
|
||||
trap 'rm -rf "$cupsTemp"' EXIT
|
||||
|
||||
restoreConfig
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly easyEffectsAppId="com.github.wwmm.easyeffects"
|
||||
readonly easyEffectsApplicationDirectory="$HOME/.var/app/com.github.wwmm.easyeffects"
|
||||
readonly easyEffectsConfig="$easyEffectsApplicationDirectory/config/easyeffects/db/easyeffectsrc"
|
||||
readonly easyEffectsArchive="$stpRoot/secrets/easyEffectsConfig.zip"
|
||||
|
||||
easyEffectsIsInstalled() {
|
||||
util::isFlatpakInstalled "$easyEffectsAppId"
|
||||
}
|
||||
|
||||
easyEffectsConfigExists() {
|
||||
[[ -f "$easyEffectsConfig" ]]
|
||||
}
|
||||
|
||||
restoreConfig() {
|
||||
log::info "Restaurando configuración desde secrets/easyEffectsConfig.zip..."
|
||||
mkdir -p "$easyEffectsApplicationDirectory"
|
||||
unzip -qo "$easyEffectsArchive" -d "$easyEffectsApplicationDirectory"
|
||||
log::ok "Configuración restaurada en: $easyEffectsApplicationDirectory"
|
||||
}
|
||||
|
||||
if ! easyEffectsIsInstalled; then
|
||||
log::info "EasyEffects no instalado aún, salteando restauración de config"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$easyEffectsArchive" ]]; then
|
||||
log::info "Sin respaldo de configuración (secrets/easyEffectsConfig.zip), salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if easyEffectsConfigExists; then
|
||||
log::info "Configuración de EasyEffects ya existe, salteando restauración"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
restoreConfig
|
||||
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
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly sshDestination="$HOME/.ssh"
|
||||
readonly encryptedArchive="$stpRoot/secrets/sshKeys.tar.gz.age"
|
||||
|
||||
sshPermissionsForFile() {
|
||||
local keyFilename="$1"
|
||||
case "$keyFilename" in
|
||||
*.pub|known_hosts|config) echo 644 ;;
|
||||
*) echo 600 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensureSshDirectoryExists() {
|
||||
if [[ ! -d "$sshDestination" ]]; then
|
||||
mkdir -p "$sshDestination"
|
||||
chmod 700 "$sshDestination"
|
||||
fi
|
||||
}
|
||||
|
||||
decryptArchiveInto() {
|
||||
local workingDirectory="$1"
|
||||
log::info "Ingresá la passphrase para descifrar las claves SSH:"
|
||||
if ! age -d -o "$workingDirectory/sshKeys.tar.gz" "$encryptedArchive"; then
|
||||
log::error "Error al descifrar. Verificá la passphrase."
|
||||
return 1
|
||||
fi
|
||||
tar -xzf "$workingDirectory/sshKeys.tar.gz" -C "$workingDirectory"
|
||||
}
|
||||
|
||||
installSshKey() {
|
||||
local sourceFile="$1"
|
||||
local keyFilename
|
||||
keyFilename="$(basename "$sourceFile")"
|
||||
local destination="$sshDestination/$keyFilename"
|
||||
|
||||
if [[ -f "$destination" ]]; then
|
||||
log::warn "Ya existe (salteando): $keyFilename"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp "$sourceFile" "$destination"
|
||||
chmod "$(sshPermissionsForFile "$keyFilename")" "$destination"
|
||||
log::ok "Instalada: $keyFilename"
|
||||
}
|
||||
|
||||
installAllKeysFrom() {
|
||||
local sourceDirectory="$1"
|
||||
local installedCount=0 skippedCount=0
|
||||
|
||||
for sourceFile in "$sourceDirectory/.ssh/"*; do
|
||||
[[ -f "$sourceFile" ]] || continue
|
||||
if installSshKey "$sourceFile"; then
|
||||
((++installedCount))
|
||||
else
|
||||
((++skippedCount))
|
||||
fi
|
||||
done
|
||||
|
||||
log::ok "$installedCount clave(s) instaladas, $skippedCount salteada(s)"
|
||||
}
|
||||
|
||||
if [[ ! -f "$encryptedArchive" ]]; then
|
||||
log::warn "Archivo de claves no encontrado: secrets/sshKeys.tar.gz.age"
|
||||
log::warn "Para cifrar tus claves actuales: bash scripts/encryptSsh.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! util::cmdExists age; then
|
||||
log::error "age no está instalado. Ejecutá primero el módulo bootstrap"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log::info "Restaurando claves SSH..."
|
||||
|
||||
workingDirectory="$(mktemp -d)"
|
||||
trap 'rm -rf "$workingDirectory"' EXIT
|
||||
|
||||
decryptArchiveInto "$workingDirectory"
|
||||
ensureSshDirectoryExists
|
||||
installAllKeysFrom "$workingDirectory"
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly thunderbirdApplicationDirectory="$HOME/snap/thunderbird/common"
|
||||
readonly thunderbirdProfileDirectory="$thunderbirdApplicationDirectory/.thunderbird"
|
||||
readonly thunderbirdArchive="$stpRoot/secrets/thunderbirdProfile.zip"
|
||||
|
||||
thunderbirdIsInstalled() {
|
||||
util::isSnapInstalled "thunderbird"
|
||||
}
|
||||
|
||||
thunderbirdProfileExists() {
|
||||
[[ -f "$thunderbirdProfileDirectory/profiles.ini" ]]
|
||||
}
|
||||
|
||||
installThunderbird() {
|
||||
sudo snap install thunderbird
|
||||
log::ok "Thunderbird instalado"
|
||||
}
|
||||
|
||||
restoreProfile() {
|
||||
log::info "Restaurando perfil desde secrets/thunderbirdProfile.zip..."
|
||||
mkdir -p "$thunderbirdApplicationDirectory"
|
||||
unzip -qo "$thunderbirdArchive" -d "$thunderbirdApplicationDirectory"
|
||||
log::ok "Perfil restaurado en: $thunderbirdProfileDirectory"
|
||||
}
|
||||
|
||||
if thunderbirdIsInstalled; then
|
||||
log::info "Thunderbird ya instalado"
|
||||
else
|
||||
installThunderbird
|
||||
fi
|
||||
|
||||
if [[ ! -f "$thunderbirdArchive" ]]; then
|
||||
log::info "Sin respaldo de perfil (secrets/thunderbirdProfile.zip), salteando restauración"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if thunderbirdProfileExists; then
|
||||
log::info "Perfil de Thunderbird ya existe, salteando restauración"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
restoreProfile
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly wireplumberStateDirectory="$HOME/.local/state/wireplumber"
|
||||
readonly wireplumberArchive="$stpRoot/secrets/wireplumberState.zip"
|
||||
|
||||
wireplumberIsInstalled() {
|
||||
util::cmdExists wireplumber
|
||||
}
|
||||
|
||||
wireplumberStateExists() {
|
||||
[[ -f "$wireplumberStateDirectory/default-nodes" ]]
|
||||
}
|
||||
|
||||
restoreState() {
|
||||
log::info "Restaurando estado desde secrets/wireplumberState.zip..."
|
||||
mkdir -p "$HOME/.local/state"
|
||||
unzip -qo "$wireplumberArchive" -d "$HOME/.local/state"
|
||||
log::ok "Estado restaurado en: $wireplumberStateDirectory"
|
||||
log::info "Reiniciá WirePlumber para aplicar los cambios: systemctl --user restart wireplumber"
|
||||
}
|
||||
|
||||
if ! wireplumberIsInstalled; then
|
||||
log::info "WirePlumber no instalado, salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$wireplumberArchive" ]]; then
|
||||
log::info "Sin respaldo de estado (secrets/wireplumberState.zip), salteando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if wireplumberStateExists; then
|
||||
log::info "Estado de WirePlumber ya existe, salteando restauración"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
restoreState
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
readonly outputFile="$stpRoot/secrets/cupsConfig.zip"
|
||||
|
||||
cupsIsInstalled() {
|
||||
util::cmdExists lpstat
|
||||
}
|
||||
|
||||
printersAreConfigured() {
|
||||
sudo test -s /etc/cups/printers.conf
|
||||
}
|
||||
|
||||
copyFilesToTemporaryDirectory() {
|
||||
local temporaryDirectory="$1"
|
||||
sudo cp /etc/cups/printers.conf "$temporaryDirectory/printers.conf"
|
||||
if sudo test -d /etc/cups/ppd && sudo test -n "$(sudo ls /etc/cups/ppd/)"; then
|
||||
sudo cp -r /etc/cups/ppd "$temporaryDirectory/ppd"
|
||||
fi
|
||||
sudo chown -R "$USER" "$temporaryDirectory"
|
||||
}
|
||||
|
||||
if ! cupsIsInstalled; then
|
||||
log::warn "CUPS no instalado"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
util::requireSudo
|
||||
|
||||
if ! printersAreConfigured; then
|
||||
log::warn "No hay impresoras configuradas en /etc/cups/printers.conf"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log::step "Capturando configuración de CUPS"
|
||||
mkdir -p "$stpRoot/secrets"
|
||||
rm -f "$outputFile"
|
||||
|
||||
temporaryDirectory="$(mktemp -d)"
|
||||
trap 'rm -rf "$temporaryDirectory"' EXIT
|
||||
|
||||
copyFilesToTemporaryDirectory "$temporaryDirectory"
|
||||
|
||||
(
|
||||
cd "$temporaryDirectory"
|
||||
find . -not -type d | sed 's|^\./||' | zip -q "$outputFile" -@
|
||||
)
|
||||
|
||||
log::ok "Guardado: secrets/cupsConfig.zip ($(du -sh "$outputFile" | cut -f1))"
|
||||
log::info "Impresoras respaldadas: $(lpstat -a | awk '{print $1}' | tr '\n' ' ')"
|
||||
log::info "Commiteá secrets/cupsConfig.zip antes de hacer push"
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$stpRoot/lib/log.sh"
|
||||
|
||||
readonly easyEffectsApplicationDirectory="$HOME/.var/app/com.github.wwmm.easyeffects"
|
||||
readonly outputFile="$stpRoot/secrets/easyEffectsConfig.zip"
|
||||
|
||||
easyEffectsDataExists() {
|
||||
[[ -d "$easyEffectsApplicationDirectory/config/easyeffects" ]] || [[ -d "$easyEffectsApplicationDirectory/data/easyeffects" ]]
|
||||
}
|
||||
|
||||
if ! easyEffectsDataExists; then
|
||||
log::warn "Datos de EasyEffects no encontrados en: $easyEffectsApplicationDirectory"
|
||||
log::info "Asegurate de haber abierto EasyEffects al menos una vez antes de capturar"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log::step "Capturando configuración de EasyEffects"
|
||||
mkdir -p "$stpRoot/secrets"
|
||||
rm -f "$outputFile"
|
||||
|
||||
(
|
||||
cd "$easyEffectsApplicationDirectory"
|
||||
find "config/easyeffects" "data/easyeffects" -not -type d 2>/dev/null \
|
||||
| zip -q "$outputFile" -@
|
||||
)
|
||||
|
||||
log::ok "Guardado: secrets/easyEffectsConfig.zip ($(du -sh "$outputFile" | cut -f1))"
|
||||
log::info "Commiteá secrets/easyEffectsConfig.zip antes de hacer push"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cifra ~/.ssh con age y guarda el resultado en secrets/sshKeys.tar.gz.age.
|
||||
# Corré esto en tu máquina actual ANTES de hacer push del repo.
|
||||
#
|
||||
# Uso: ./scripts/encryptSsh.sh [ruta-a-.ssh]
|
||||
set -euo pipefail
|
||||
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
sshSource="${1:-$HOME/.ssh}"
|
||||
|
||||
if [[ ! -d "$sshSource" ]]; then
|
||||
echo "Error: directorio no encontrado: $sshSource"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v age &>/dev/null; then
|
||||
echo "Error: age no está instalado. Instalalo con: sudo apt install age"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$stpRoot/secrets"
|
||||
readonly outputFile="$stpRoot/secrets/sshKeys.tar.gz.age"
|
||||
|
||||
workingDirectory="$(mktemp -d)"
|
||||
trap 'rm -rf "$workingDirectory"' EXIT
|
||||
|
||||
echo "Empaquetando $sshSource..."
|
||||
tar -czf "$workingDirectory/sshKeys.tar.gz" -C "$(dirname "$sshSource")" "$(basename "$sshSource")"
|
||||
|
||||
echo "Cifrando con age (usá una passphrase segura que puedas recordar)..."
|
||||
age --passphrase --output "$outputFile" "$workingDirectory/sshKeys.tar.gz"
|
||||
|
||||
echo ""
|
||||
echo "✓ Claves cifradas guardadas en: secrets/sshKeys.tar.gz.age"
|
||||
echo " Este archivo es seguro para commitear — está cifrado simétricamente."
|
||||
echo ""
|
||||
echo "Para verificar que el cifrado funciona:"
|
||||
echo " age --decrypt secrets/sshKeys.tar.gz.age | tar -tzv"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Excluye emails, caché, crash dumps y archivos de bloqueo.
|
||||
# Replica lo que hace el export integrado de Thunderbird (Ajustes → Exportar perfil).
|
||||
set -euo pipefail
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$stpRoot/lib/log.sh"
|
||||
|
||||
readonly thunderbirdApplicationDirectory="$HOME/snap/thunderbird/common"
|
||||
readonly thunderbirdProfileDirectory="$thunderbirdApplicationDirectory/.thunderbird"
|
||||
readonly outputFile="$stpRoot/secrets/thunderbirdProfile.zip"
|
||||
|
||||
thunderbirdDataExists() {
|
||||
[[ -d "$thunderbirdProfileDirectory" ]]
|
||||
}
|
||||
|
||||
if ! thunderbirdDataExists; then
|
||||
log::warn "Datos de Thunderbird Snap no encontrados en: $thunderbirdProfileDirectory"
|
||||
log::info "Asegurate de haber abierto Thunderbird al menos una vez antes de capturar"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log::step "Capturando configuración de Thunderbird"
|
||||
mkdir -p "$stpRoot/secrets"
|
||||
rm -f "$outputFile"
|
||||
|
||||
(
|
||||
cd "$thunderbirdApplicationDirectory"
|
||||
find ".thunderbird" \
|
||||
| grep -Ev "/(Mail|ImapMail|News|cache|cache2|startupCache|minidumps|crashes|storage|Crash Reports)(/|$)" \
|
||||
| grep -Ev "\.(msf|sqlite-wal|sqlite-shm)$" \
|
||||
| grep -Ev "/(parent\.lock|lock)$" \
|
||||
| zip -q "$outputFile" -@
|
||||
)
|
||||
|
||||
log::ok "Guardado: secrets/thunderbirdProfile.zip ($(du -sh "$outputFile" | cut -f1))"
|
||||
log::info "Commiteá secrets/thunderbirdProfile.zip antes de hacer push"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$stpRoot/lib/log.sh"
|
||||
|
||||
readonly wireplumberStateDirectory="$HOME/.local/state/wireplumber"
|
||||
readonly outputFile="$stpRoot/secrets/wireplumberState.zip"
|
||||
|
||||
wireplumberStateExists() {
|
||||
[[ -d "$wireplumberStateDirectory" ]]
|
||||
}
|
||||
|
||||
if ! wireplumberStateExists; then
|
||||
log::warn "Estado de WirePlumber no encontrado en: $wireplumberStateDirectory"
|
||||
log::info "Asegurate de haber conectado los auriculares bluetooth al menos una vez"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log::step "Capturando estado de WirePlumber"
|
||||
mkdir -p "$stpRoot/secrets"
|
||||
rm -f "$outputFile"
|
||||
|
||||
(
|
||||
cd "$HOME/.local/state"
|
||||
find "wireplumber" -not -type d | zip -q "$outputFile" -@
|
||||
)
|
||||
|
||||
log::ok "Guardado: secrets/wireplumberState.zip ($(du -sh "$outputFile" | cut -f1))"
|
||||
log::info "Commiteá secrets/wireplumberState.zip antes de hacer push"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
stpRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export stpRoot
|
||||
|
||||
source "$stpRoot/lib/log.sh"
|
||||
source "$stpRoot/lib/utils.sh"
|
||||
|
||||
export verbose="${verbose:-false}"
|
||||
|
||||
selectedModule=""
|
||||
skipModules=()
|
||||
|
||||
readModuleOrder() {
|
||||
grep -v '^\s*#' "$stpRoot/config/modules" | grep -v '^\s*$'
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Uso: stp.sh [opciones]
|
||||
|
||||
Opciones:
|
||||
-m, --module <nombre> Ejecutar solo este módulo (ej: packages)
|
||||
-s, --skip <nombre> Saltear módulo, puede repetirse
|
||||
-v, --verbose Salida detallada
|
||||
-l, --list Listar módulos en orden de ejecución
|
||||
-h, --help Mostrar esta ayuda
|
||||
|
||||
Ejemplos:
|
||||
stp.sh # Ejecutar todos los módulos
|
||||
stp.sh -m packages # Solo instalar paquetes
|
||||
stp.sh -s video -s audio # Saltear video y audio
|
||||
EOF
|
||||
}
|
||||
|
||||
listModules() {
|
||||
echo "Módulos (en orden de ejecución):"
|
||||
while IFS= read -r name; do
|
||||
local modulePath="$stpRoot/modules/${name}.sh"
|
||||
if [[ -f "$modulePath" ]]; then
|
||||
printf " %s\n" "$name"
|
||||
else
|
||||
printf " %s (⚠ no encontrado: modules/%s.sh)\n" "$name" "$name"
|
||||
fi
|
||||
done < <(readModuleOrder)
|
||||
}
|
||||
|
||||
shouldSkip() {
|
||||
local moduleName="$1"
|
||||
for skip in "${skipModules[@]:-}"; do
|
||||
[[ "$moduleName" == "$skip" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
runModule() {
|
||||
local moduleName="$1"
|
||||
local modulePath="$stpRoot/modules/${moduleName}.sh"
|
||||
|
||||
if [[ ! -f "$modulePath" ]]; then
|
||||
log::error "Módulo no encontrado: modules/${moduleName}.sh"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if shouldSkip "$moduleName"; then
|
||||
log::warn "Salteando: $moduleName"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::step "$moduleName"
|
||||
if ! bash "$modulePath"; then
|
||||
log::error "Falló el módulo: $moduleName (ver $stpLogFile)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-m|--module) selectedModule="$2"; shift 2 ;;
|
||||
-s|--skip) skipModules+=("$2"); shift 2 ;;
|
||||
-v|--verbose) verbose=true; shift ;;
|
||||
-l|--list) listModules; exit 0 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) log::error "Opción desconocida: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
log::step "Sistema de Transferencia Personal"
|
||||
log::info "Log: $stpLogFile"
|
||||
printf '\n'
|
||||
|
||||
util::requireSudo
|
||||
|
||||
if [[ -n "$selectedModule" ]]; then
|
||||
if [[ ! -f "$stpRoot/modules/${selectedModule}.sh" ]]; then
|
||||
log::error "Módulo no encontrado: $selectedModule"
|
||||
listModules
|
||||
exit 1
|
||||
fi
|
||||
runModule "$selectedModule"
|
||||
else
|
||||
while IFS= read -r moduleName; do
|
||||
runModule "$moduleName"
|
||||
printf '\n'
|
||||
done < <(readModuleOrder)
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
log::ok "STP completado. Log completo en: $stpLogFile"
|
||||
Reference in New Issue
Block a user