# 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)
- `docker` — installs Docker Engine, Docker Compose plugin, adds user to `docker` group, and installs Lazydocker
- `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`
