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:
2026-06-20 18:16:40 -03:00
parent ff401597be
commit bd78fd9fbe
34 changed files with 2348 additions and 2 deletions
+205
View File
@@ -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`
+20
View File
@@ -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
```
+47
View File
@@ -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`
+18
View File
@@ -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:*)"
]
}
}
+146
View File
@@ -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
```
+7
View File
@@ -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
+187
View File
@@ -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`
+260 -2
View File
@@ -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
View File
@@ -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" "$@"
+29
View File
@@ -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-----
+12
View File
@@ -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
+180
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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}" " ✗ " "$@"; }
+40
View File
@@ -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
View File
@@ -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" ]]
}
+82
View File
@@ -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
+37
View File
@@ -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
+55
View File
@@ -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
+41
View File
@@ -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
+414
View File
@@ -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
View File
@@ -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"
+46
View File
@@ -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
+40
View File
@@ -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
+54
View File
@@ -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"
+30
View File
@@ -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"
+38
View File
@@ -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"
+36
View File
@@ -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"
+29
View File
@@ -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.
Executable
+111
View File
@@ -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"