heartwood every commit a ring

the laptop puts down roots in the volume, helpers gather under scripts/, each leaf knows its tree

f8bd9166 by Isaac Bythewood · 8 days ago

modified CLAUDE.md
@@ -8,7 +8,13 @@ Taproot is a personal infrastructure repository containing dotfiles, a Docker de## Key Commands**Build the development container:****Set up the dev container on a Windows host (idempotent, safe to re-run):**```powershell.\containers\webdev\bootstrap.ps1 desktop      # or "laptop".\containers\webdev\bootstrap.ps1 laptop -Restore   # also pulls B2 snapshot```**Build the dev container manually (bootstrap.ps1 does this for you):**```shdocker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .```
@@ -25,10 +31,22 @@ sh hosts/alpine/srv/bootstrap.shThere are no tests, linters, or build steps in this repo — it is pure configuration.## Helper Scripts (inside the webdev container, in PATH at `~/scripts/`)| Command | What it does ||---|---|| `backup` | Manual restic backup to B2 (tags snapshot with `$RESTIC_HOST` from `~/.restic/b2-env`) || `restore` | Pull latest snapshot from B2 into volumes; existing data archived first || `sync` | `git fetch` + `git pull --ff-only` for every repo under `~/code/`; skips dirty/divergent || `status` | Last snapshot per host across both webdev and alpine restic repos, plus repo size |## Architecture- **`dotfiles/`** — Terminal and editor config (bash, git, tmux, neovim). Neovim config is Lua-based with a custom statusline. These get COPYed into the container at build time.- **`containers/webdev/`** — Ubuntu 24.04 dev container with Node 22, Python 3 (pip + uv), Bun, Docker-in-Docker, Playwright Chromium (under `/opt/playwright-browsers`, for the Claude playwright MCP), and standard dev tools (neovim, tmux, git, rsync, htop, nmap, unzip, etc.). Stays alive via `sleep infinity`; entered through `docker exec -it ... tmux` for the TUI workflow or over SSH on host port 2222 for editor remote-dev. `entrypoint.sh` starts sshd before exec'ing CMD; host keys persist in the `bythewood-ssh` volume so fingerprints survive rebuilds. Started with `docker run --init` so PID 1 reaps zombies left behind when tmux/sshd children exit.- **`dotfiles/host/`** — Host-side configs that don't belong in the container: Zed editor settings and Windows SSH config. These are NOT placed automatically by bootstrap.ps1; copy them manually on a fresh machine:  - `dotfiles/host/zed-settings.json` -> `%APPDATA%\Zed\settings.json`  - `dotfiles/host/ssh-config` -> `~\.ssh\config` (merge into existing entries if you have other Hosts already configured)- **`containers/webdev/`** — Ubuntu 24.04 dev container with Node 22, Python 3 (pip + uv), Bun, Docker CLI, Playwright Chromium (under `/opt/playwright-browsers`, for the Claude playwright MCP), and standard dev tools (neovim, tmux, git, rsync, htop, nmap, unzip, etc.). Stays alive via `sleep infinity`; entered through `docker exec -it ... tmux` for the TUI workflow or over SSH on host port 2222 for editor remote-dev. `entrypoint.sh` starts sshd before exec'ing CMD; host keys persist in the `bythewood-ssh` volume so fingerprints survive rebuilds. Started with `docker run --init` so PID 1 reaps zombies left behind when tmux/sshd children exit. Helper scripts (`backup`, `restore`, `sync`, `status`) are baked in at `/home/dev/scripts/` and on PATH. Host setup is automated by `bootstrap.ps1`.- **`hosts/alpine/`** — Production server setup: Caddy reverse proxy (auto HTTPS), Docker Compose for services, restic backups to Backblaze B2, UFW firewall, push-to-deploy via git hooks.## Deployed Projects
modified containers/webdev/Dockerfile
@@ -1,87 +1,38 @@# webdev## Used for working on most of my website development projects, using a rolling# distro to have the latest packages to work with. Uses tmux, neovim, and# claude code for a full TUI workflow.## NOTE: Build from the taproot repo root so dotfiles are in the build context:# Ubuntu 24.04 dev container with tmux, neovim, claude code, Bun, uv, Docker# CLI, and Playwright Chromium. Used as a portable development environment# across desktop and laptop, with restic backups to Backblaze B2.## Setup is automated by bootstrap.ps1 (idempotent, run from PowerShell on the# host). It creates volumes, builds this image, runs the container, copies# the host SSH key in, and prompts for restic creds:##     cd <path-to-taproot>#     .\containers\webdev\bootstrap.ps1 desktop      # or "laptop"#     .\containers\webdev\bootstrap.ps1 laptop -Restore   # also pulls B2 snapshot## Connect (TUI):    docker exec -it bythewood-webdev tmux# Connect (SSH):    ssh -p 2222 dev@localhost## Helper scripts inside the container (in PATH at /home/dev/scripts/):#     backup     manual restic backup to B2#     restore    pull latest snapshot from B2#     sync       git fetch + ff-only pull every repo under ~/code/#     status     last snapshot per host across both restic repos## The bythewood-* volumes survive image rebuilds, so /home/dev/code,# /home/dev/.claude, /home/dev/.ssh, and /home/dev/.restic persist. Fresh# volumes inherit dev:dev ownership from the image. If existing volumes ever# hit root:root issues:#     docker exec -it bythewood-webdev sudo chown -R dev:dev \#         /home/dev/.claude /home/dev/code /home/dev/.restic## Manual build (bootstrap.ps1 does this for you):#     docker build --tag overshard/webdev:latest -f containers/webdev/Dockerfile .## Create volumes:#     docker volume create --name bythewood-code#     docker volume create --name bythewood-claude#     docker volume create --name bythewood-ssh#     docker volume create --name bythewood-restic## Start container (--init gives us a proper PID 1 that reaps zombies — without# it, exiting tmux leaves [tmux: server] <defunct> entries hanging around. Port# 2222 on the host maps to sshd inside the container for editor remote-dev):#     docker run --detach --init --restart unless-stopped --name bythewood-webdev \#         --volume bythewood-code:/home/dev/code \#         --volume bythewood-claude:/home/dev/.claude \#         --volume bythewood-ssh:/home/dev/.ssh \#         --volume bythewood-restic:/home/dev/.restic \#         --volume /var/run/docker.sock:/var/run/docker.sock \#         -p 8000:8000 -p 2222:22 \#         overshard/webdev:latest## Copy SSH keys into the volume (first time only, PowerShell):#     docker cp $HOME/.ssh/home_key bythewood-webdev:/home/dev/.ssh/home_key#     docker cp $HOME/.ssh/home_key.pub bythewood-webdev:/home/dev/.ssh/home_key.pub#     docker exec bythewood-webdev sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pub#     docker exec bythewood-webdev chmod 600 /home/dev/.ssh/home_key## The entrypoint symlinks home_key.pub -> authorized_keys on first start, so# the same key authenticates both outbound (from the container) and inbound# (from your host into the container). To pull the public key back out:#     docker cp bythewood-webdev:/home/dev/.ssh/home_key.pub - | tar -xO## Connect (TUI workflow):#     docker exec -it bythewood-webdev tmux## Connect (SSH for editor remote-dev — Zed, VS Code, JetBrains Gateway):#     1. Add this entry to your host machine's ~/.ssh/config:#            Host bythewood-webdev#                HostName localhost#                Port 2222#                User dev#                IdentityFile ~/.ssh/home_key#                StrictHostKeyChecking accept-new#     2. ssh bythewood-webdev   (or use bythewood-webdev as the remote in Zed)## Host keys persist in /home/dev/.ssh/host_keys/ inside the bythewood-ssh# volume, so SSH fingerprints survive image rebuilds.## Restic credentials (first time only) — pull from 1Password:#     docker exec -i bythewood-webdev tee /home/dev/.restic/password > /dev/null  # paste, Ctrl-D#     docker exec -i bythewood-webdev tee /home/dev/.restic/b2-env > /dev/null    # paste, Ctrl-D#     docker exec bythewood-webdev chmod 600 /home/dev/.restic/password /home/dev/.restic/b2-env## b2-env contents:#     export B2_ACCOUNT_ID="<keyID from 1Password>"#     export B2_ACCOUNT_KEY="<applicationKey from 1Password>"## Backup and restore:#     docker exec -it bythewood-webdev /home/dev/backup.sh#     docker exec -it bythewood-webdev /home/dev/restore.sh## I use volumes for code, claude, ssh, and restic to make rebuilds of the# container easy without losing project files, claude auth/memory, SSH keys,# or restic credentials. I have scripts setup on my hosts to rebuild images,# delete old containers, and start the new containers when I make updates.## NOTE: Fresh volumes will inherit correct dev:dev ownership from the image.# Existing volumes retain their permissions. If you hit root:root ownership# issues on existing volumes, fix with:#     docker exec -it bythewood-webdev sudo chown -R dev:dev /home/dev/.claude /home/dev/code /home/dev/.restic## Docker is included in this build so I can test docker stuff inside my# containers. Note that to get docker to function in a docker container you# need to use the host's docker and provide the docker.sock file from the host# to the container. This can be done with `--volume /var/run/docker.sock:/var/run/docker.sock`.## The naming is so that I can make a container and volumes for each of my# clients and keep a separation of work.# The docker CLI is installed and /var/run/docker.sock is mounted in so I can# poke at the host's daemon from inside the container when I need to.FROM ubuntu:24.04
@@ -157,10 +108,12 @@ RUN mkdir -p /home/dev/code /home/dev/.claude /home/dev/.ssh /home/dev/.restic &    chmod 600 /home/dev/.ssh/config && \    chown -R dev:dev /home/dev/code /home/dev/.claude /home/dev/.ssh /home/dev/.resticCOPY containers/webdev/backup.sh /home/dev/backup.shCOPY containers/webdev/restore.sh /home/dev/restore.shRUN chmod +x /home/dev/backup.sh /home/dev/restore.sh && \    chown dev:dev /home/dev/backup.sh /home/dev/restore.shCOPY containers/webdev/backup.sh /home/dev/scripts/backupCOPY containers/webdev/restore.sh /home/dev/scripts/restoreCOPY containers/webdev/sync.sh /home/dev/scripts/syncCOPY containers/webdev/status.sh /home/dev/scripts/statusRUN chmod +x /home/dev/scripts/* && \    chown -R dev:dev /home/dev/scripts# sshd setup. Host keys are NOT generated here — the entrypoint creates them# in the .ssh volume on first start so fingerprints survive image rebuilds.
modified containers/webdev/backup.sh
@@ -6,9 +6,18 @@# Initializes the repo on first run. Prunes per the retention policy# (7 daily, 4 weekly, 6 monthly) after each successful backup.## Snapshots are tagged with --host so desktop and laptop snapshots stay# distinct in the shared repo. Retention is applied per (host, paths),# so each machine gets its own 7d/4w/6m window.## Credentials live in ~/.restic/ (mounted from the bythewood-restic volume):#   ~/.restic/password   restic repo password (0600)#   ~/.restic/b2-env     exports B2_ACCOUNT_ID and B2_ACCOUNT_KEY (0600)#   ~/.restic/b2-env     exports B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_HOST (0600)## b2-env example:#   export B2_ACCOUNT_ID="..."#   export B2_ACCOUNT_KEY="..."#   export RESTIC_HOST="desktop"   # or "laptop"#set -eu
@@ -17,6 +26,12 @@ set -euexport RESTIC_REPOSITORY="b2:overshard-backups:webdev"export RESTIC_PASSWORD_FILE="$HOME/.restic/password"if [ -z "${RESTIC_HOST:-}" ]; then    echo "ERROR: RESTIC_HOST is not set." >&2    echo "Add 'export RESTIC_HOST=desktop' (or laptop) to ~/.restic/b2-env" >&2    exit 1fiif ! restic cat config >/dev/null 2>&1; then    echo "Repository not initialized. Running restic init..."    restic init
@@ -24,7 +39,9 @@ firestic backup \    --verbose \    --host="$RESTIC_HOST" \    --exclude-caches \    --exclude='host_keys' \    --exclude='node_modules' \    --exclude='.next' \    --exclude='.venv' \
@@ -39,6 +56,7 @@ restic backup \    "$HOME/.ssh"restic forget --prune \    --host="$RESTIC_HOST" \    --keep-daily   7 \    --keep-weekly  4 \    --keep-monthly 6
added containers/webdev/bootstrap.ps1
@@ -0,0 +1,369 @@<#.SYNOPSIS    Idempotent host-side setup for the bythewood-webdev Docker container..DESCRIPTION    Brings a Windows 11 machine from "Docker Desktop installed + SSH key set up"    to a fully running webdev container with restic credentials in place.    The host stays minimal: this script never clones taproot to the host    filesystem. Instead it uses one-shot ubuntu:24.04 helper containers (the    same base image webdev itself is built from) to do the work directly    against the bythewood-code volume:        - clone taproot into the volume via SSH (using the host's home_key,          mounted read-only)        - build the webdev image using docker.sock to reach the host's daemon    Each step inspects current state and skips if already done, so this is    safe to re-run on a partially-configured machine.    First-time machine setup (one-liner; assumes Docker Desktop is running and    you have an SSH key at $HOME/.ssh/home_key added to GitHub):        irm https://raw.githubusercontent.com/overshard/taproot/master/containers/webdev/bootstrap.ps1 -OutFile bootstrap.ps1        .\bootstrap.ps1 laptop    Host-side dotfiles (Zed settings, ~/.ssh/config) are NOT managed by this    script. After taproot is in the bythewood-code volume, copy them by hand:        dotfiles/host/zed-settings.json -> $env:APPDATA\Zed\settings.json        dotfiles/host/ssh-config        -> $HOME\.ssh\config    Subsequent runs (from inside the cloned-in-volume taproot is fine; the    script doesn't depend on its own location):        .\bootstrap.ps1 desktop                # idempotent, all skips        .\bootstrap.ps1 laptop -Force          # pull taproot, rebuild image, recreate container        .\bootstrap.ps1 laptop -Restore        # restore data from B2 snapshot.PARAMETER HostTag    The restic --host tag for this machine. Use "desktop" or "laptop"..PARAMETER Only    Run only the named step. One of: prereqs, volumes, taproot, image,    container, ssh, restic-password, b2-env, alpine-password, restore..PARAMETER Restore    After all setup steps, run ~/scripts/restore inside the container to pull    data from the latest B2 snapshot. Opt-in..PARAMETER Force    Pull latest taproot, rebuild the image, remove the existing container,    create a new container from the new image. Volumes (and your data) are    untouched..PARAMETER TaprootRepo    SSH URL of the taproot repository. Default: git@github.com:overshard/taproot.git.PARAMETER TaprootBranch    Branch to clone/pull. Default: master#>[CmdletBinding()]param(    [Parameter(Mandatory=$true, Position=0)]    [ValidateSet("desktop", "laptop")]    [string]$HostTag,    [ValidateSet(        "prereqs", "volumes", "taproot", "image", "container",        "ssh", "restic-password", "b2-env", "alpine-password", "restore"    )]    [string]$Only,    [switch]$Restore,    [switch]$Force,    [string]$TaprootRepo = "git@github.com:overshard/taproot.git",    [string]$TaprootBranch = "master")$ErrorActionPreference = "Stop"$ImageName = "overshard/webdev:latest"$ContainerName = "bythewood-webdev"$Volumes = @("bythewood-code", "bythewood-claude", "bythewood-ssh", "bythewood-restic")$HelperImage = "ubuntu:24.04"$HostKeyPath = Join-Path $HOME ".ssh\home_key"$HostKeyPubPath = Join-Path $HOME ".ssh\home_key.pub"function Step-Banner { param([string]$Name) Write-Host ""; Write-Host "=== $Name ===" -ForegroundColor Cyan }function Done   { param([string]$M) Write-Host "  [done] $M" -ForegroundColor Green }function Skip   { param([string]$M) Write-Host "  [skip] $M" -ForegroundColor DarkGray }function Warn   { param([string]$M) Write-Host "  [warn] $M" -ForegroundColor Yellow }function Fail   { param([string]$M) Write-Host "  [fail] $M" -ForegroundColor Red; exit 1 }function Should-Run { param([string]$Name) return (-not $Only) -or ($Only -eq $Name) }# ---------------------------------------------------------------------------function Step-Prereqs {    Step-Banner "prereqs"    try {        $null = docker version --format '{{.Server.Version}}' 2>$null        Done "Docker Desktop is running"    } catch {        Fail "Docker Desktop is not running. Start it and re-run."    }    if (-not (Test-Path $HostKeyPath) -or -not (Test-Path $HostKeyPubPath)) {        Fail @"SSH key not found at $HostKeyPath (.pub).Set up your keys first:    ssh-keygen -t ed25519 -f `"$HostKeyPath`" -C bythewood-webdevThen add the public key to GitHub:    Get-Content `"$HostKeyPubPath`" | clipPaste at https://github.com/settings/ssh/new"@    }    Done "SSH key found at $HostKeyPath"}# ---------------------------------------------------------------------------function Step-Volumes {    Step-Banner "volumes"    foreach ($v in $Volumes) {        docker volume inspect $v 2>$null 1>$null        if ($LASTEXITCODE -eq 0) {            Skip "$v exists"        } else {            docker volume create --name $v | Out-Null            Done "created $v"        }    }}# ---------------------------------------------------------------------------function Invoke-Helper-Clone {    param([string]$Action) # "clone" or "pull"    $cmd = if ($Action -eq "clone") {        "git clone --branch '$TaprootBranch' '$TaprootRepo' /code/taproot"    } else {        "cd /code/taproot && git fetch --all --prune && git pull --ff-only"    }    docker run --rm `        --volume "${HostKeyPath}:/keys/home_key:ro" `        --volume "bythewood-code:/code" `        -e GIT_SSH_COMMAND="ssh -i /keys/home_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/known_hosts" `        $HelperImage `        sh -c "set -e; apt-get update >/dev/null && apt-get install -y --no-install-recommends git openssh-client >/dev/null && $cmd"    if ($LASTEXITCODE -ne 0) { Fail "helper container failed during '$Action'" }}function Step-Taproot {    Step-Banner "taproot"    docker run --rm --volume "bythewood-code:/code" $HelperImage `        test -d /code/taproot/.git 2>$null    $exists = ($LASTEXITCODE -eq 0)    if (-not $exists) {        Done "cloning $TaprootRepo into bythewood-code volume"        Invoke-Helper-Clone -Action "clone"        return    }    if ($Force) {        Done "pulling latest $TaprootBranch into existing /code/taproot"        Invoke-Helper-Clone -Action "pull"        return    }    Skip "taproot already in bythewood-code volume (use -Force to git pull)"}# ---------------------------------------------------------------------------function Step-Image {    Step-Banner "image"    docker image inspect $ImageName 2>$null 1>$null    $exists = ($LASTEXITCODE -eq 0)    if ($exists -and -not $Force) {        Skip "$ImageName already built (use -Force to rebuild)"        return    }    Done "building $ImageName from /code/taproot in bythewood-code"    docker run --rm `        --volume "bythewood-code:/code" `        --volume "/var/run/docker.sock:/var/run/docker.sock" `        $HelperImage `        sh -c "set -e; apt-get update >/dev/null && apt-get install -y --no-install-recommends docker.io >/dev/null && cd /code/taproot && docker build --tag $ImageName -f containers/webdev/Dockerfile ."    if ($LASTEXITCODE -ne 0) { Fail "image build failed" }}# ---------------------------------------------------------------------------function Step-Container {    Step-Banner "container"    docker container inspect $ContainerName 2>$null 1>$null    $exists = ($LASTEXITCODE -eq 0)    if ($exists -and $Force) {        Done "removing existing container (-Force)"        docker rm -f $ContainerName | Out-Null        $exists = $false    }    if ($exists) {        $running = (docker container inspect -f '{{.State.Running}}' $ContainerName)        if ($running -eq "true") {            Skip "$ContainerName running"        } else {            docker start $ContainerName | Out-Null            Done "started existing $ContainerName"        }        return    }    Done "creating $ContainerName"    $dockerArgs = @(        "run", "--detach", "--init", "--restart", "unless-stopped",        "--name", $ContainerName,        "--volume", "bythewood-code:/home/dev/code",        "--volume", "bythewood-claude:/home/dev/.claude",        "--volume", "bythewood-ssh:/home/dev/.ssh",        "--volume", "bythewood-restic:/home/dev/.restic",        "--volume", "/var/run/docker.sock:/var/run/docker.sock",        "-p", "8000:8000",        "-p", "2222:22",        $ImageName    )    docker @dockerArgs | Out-Null    if ($LASTEXITCODE -ne 0) { Fail "docker run failed" }}# ---------------------------------------------------------------------------function Step-Ssh {    Step-Banner "ssh"    docker exec $ContainerName test -f /home/dev/.ssh/home_key 2>$null    $keyOk = ($LASTEXITCODE -eq 0)    docker exec $ContainerName test -f /home/dev/.ssh/home_key.pub 2>$null    $pubOk = ($LASTEXITCODE -eq 0)    if ($keyOk -and $pubOk) {        Skip "home_key already in volume"        return    }    docker cp $HostKeyPath "${ContainerName}:/home/dev/.ssh/home_key" | Out-Null    docker cp $HostKeyPubPath "${ContainerName}:/home/dev/.ssh/home_key.pub" | Out-Null    docker exec $ContainerName sudo chown dev:dev /home/dev/.ssh/home_key /home/dev/.ssh/home_key.pub | Out-Null    docker exec $ContainerName chmod 600 /home/dev/.ssh/home_key | Out-Null    Done "copied home_key + home_key.pub into bythewood-ssh volume"}# ---------------------------------------------------------------------------function Read-Secret { param([string]$Prompt)    $sec = Read-Host -AsSecureString -Prompt $Prompt    return [System.Net.NetworkCredential]::new("", $sec).Password}function Write-File-Into-Container { param([string]$Path, [string]$Content)    $tmp = New-TemporaryFile    try {        $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Content)        [System.IO.File]::WriteAllBytes($tmp.FullName, $bytes)        docker cp $tmp.FullName "${ContainerName}:${Path}" | Out-Null        docker exec $ContainerName sudo chown dev:dev $Path | Out-Null        docker exec $ContainerName chmod 600 $Path | Out-Null    } finally {        Remove-Item $tmp.FullName -Force    }}# ---------------------------------------------------------------------------function Step-ResticPassword {    Step-Banner "restic-password"    docker exec $ContainerName test -s /home/dev/.restic/password 2>$null    if ($LASTEXITCODE -eq 0) {        Skip "/home/dev/.restic/password already set"        return    }    $pw = Read-Secret "Restic webdev repo password (paste from 1Password)"    if (-not $pw) { Fail "empty password" }    Write-File-Into-Container "/home/dev/.restic/password" "$pw`n"    Done "wrote /home/dev/.restic/password"}# ---------------------------------------------------------------------------function Step-B2Env {    Step-Banner "b2-env"    docker exec $ContainerName test -s /home/dev/.restic/b2-env 2>$null    $exists = ($LASTEXITCODE -eq 0)    if ($exists) {        $currentText = (docker exec $ContainerName cat /home/dev/.restic/b2-env) -join "`n"        if ($currentText -match 'RESTIC_HOST=["]?([^"\r\n]+)["]?') {            $existingHost = $matches[1].Trim('"')            if ($existingHost -eq $HostTag) {                Skip "/home/dev/.restic/b2-env complete (RESTIC_HOST=$existingHost)"            } else {                Warn "/home/dev/.restic/b2-env has RESTIC_HOST=$existingHost (expected $HostTag)."                Warn "Not modifying. Edit manually if wrong: docker exec -it $ContainerName nvim /home/dev/.restic/b2-env"            }            return        }        Done "appending RESTIC_HOST=$HostTag to existing b2-env"        $newContent = $currentText.TrimEnd("`r","`n") + "`nexport RESTIC_HOST=`"$HostTag`"`n"        Write-File-Into-Container "/home/dev/.restic/b2-env" $newContent        return    }    $accountId = Read-Secret "B2 account ID"    $accountKey = Read-Secret "B2 account key"    if (-not $accountId -or -not $accountKey) { Fail "empty B2 credential" }    $content = @"export B2_ACCOUNT_ID="$accountId"export B2_ACCOUNT_KEY="$accountKey"export RESTIC_HOST="$HostTag""@    Write-File-Into-Container "/home/dev/.restic/b2-env" "$content`n"    Done "wrote /home/dev/.restic/b2-env with RESTIC_HOST=$HostTag"}# ---------------------------------------------------------------------------function Step-AlpinePassword {    Step-Banner "alpine-password"    docker exec $ContainerName test -s /home/dev/.restic/alpine-password 2>$null    if ($LASTEXITCODE -eq 0) {        Skip "/home/dev/.restic/alpine-password already set"        return    }    Write-Host "  Optional: lets ~/scripts/status query the alpine repo too." -ForegroundColor DarkGray    $pw = Read-Secret "Alpine repo password (or empty to skip)"    if (-not $pw) {        Skip "no value provided"        return    }    Write-File-Into-Container "/home/dev/.restic/alpine-password" "$pw`n"    Done "wrote /home/dev/.restic/alpine-password"}# ---------------------------------------------------------------------------function Step-Restore {    Step-Banner "restore"    if (-not $Restore -and $Only -ne "restore") {        Skip "use -Restore to pull data from B2"        return    }    Done "running ~/scripts/restore inside container"    docker exec -it $ContainerName /home/dev/scripts/restore}# ---------------------------------------------------------------------------if (Should-Run "prereqs")          { Step-Prereqs }if (Should-Run "volumes")          { Step-Volumes }if (Should-Run "taproot")          { Step-Taproot }if (Should-Run "image")            { Step-Image }if (Should-Run "container")        { Step-Container }if (Should-Run "ssh")              { Step-Ssh }if (Should-Run "restic-password")  { Step-ResticPassword }if (Should-Run "b2-env")           { Step-B2Env }if (Should-Run "alpine-password")  { Step-AlpinePassword }if (Should-Run "restore")          { Step-Restore }Write-Host ""Write-Host "Done." -ForegroundColor GreenWrite-Host "Connect with:  docker exec -it $ContainerName tmux"Write-Host "Or via SSH:    ssh -p 2222 dev@localhost"
added containers/webdev/status.sh
@@ -0,0 +1,65 @@#!/bin/sh## status.sh## Show backup health for both restic repos (webdev and alpine) from a single# command. Both repos live in the same Backblaze B2 account, so the B2 creds# in ~/.restic/b2-env work for both; only the per-repo password file differs.## Files used:#   ~/.restic/b2-env           B2 account creds (required)#   ~/.restic/password         webdev repo password (required)#   ~/.restic/alpine-password  alpine repo password (optional; warns if missing)#set -u. "$HOME/.restic/b2-env"# Strip RESTIC_REPOSITORY/RESTIC_PASSWORD_FILE from any inherited env so we# can set them per-repo below.unset RESTIC_REPOSITORYunset RESTIC_PASSWORD_FILENOW=$(date -u +'%Y-%m-%d %H:%M:%S UTC')echo "Now: $NOW"echo ""show_repo() {    label=$1    repo=$2    pwfile=$3    echo "=== $label ($repo) ==="    if [ ! -s "$pwfile" ]; then        echo "  password file missing or empty: $pwfile"        echo "  (place the repo password there, chmod 600)"        echo ""        return    fi    export RESTIC_REPOSITORY="$repo"    export RESTIC_PASSWORD_FILE="$pwfile"    if ! restic cat config >/dev/null 2>&1; then        echo "  cannot open repository (check creds / network)"        echo ""        return    fi    echo ""    echo "  Latest snapshot per host:"    restic snapshots --latest 1 --group-by host --compact 2>/dev/null \        | sed 's/^/    /'    echo ""    echo "  Repo size:"    restic stats --mode raw-data 2>/dev/null \        | grep -E 'Total (Size|Blob Count|Uncompressed Size)|Compression Ratio' \        | sed 's/^[[:space:]]*/    /'    echo ""}show_repo "webdev" "b2:overshard-backups:webdev" "$HOME/.restic/password"show_repo "alpine" "b2:overshard-backups:alpine" "$HOME/.restic/alpine-password"
added containers/webdev/sync.sh
@@ -0,0 +1,67 @@#!/bin/sh## sync.sh## Pull latest from origin for every git repo under ~/code/. Skips repos with# uncommitted changes. Uses --ff-only so divergent branches never get a silent# merge or rebase; they just warn and move on.## Run after switching machines (e.g. desktop -> laptop) to catch up.#set -uCODE="$HOME/code"if [ ! -d "$CODE" ]; then    echo "ERROR: $CODE does not exist" >&2    exit 1fiok=0warn=0skip=0for path in "$CODE"/*/; do    [ -d "$path/.git" ] || continue    name=$(basename "$path")    cd "$path"    if [ -n "$(git status --porcelain)" ]; then        printf "  [skip] %s (dirty)\n" "$name"        skip=$((skip + 1))        continue    fi    if ! git symbolic-ref --quiet HEAD >/dev/null; then        printf "  [skip] %s (detached HEAD)\n" "$name"        skip=$((skip + 1))        continue    fi    if ! git fetch --all --prune --quiet 2>/dev/null; then        printf "  [warn] %s (fetch failed)\n" "$name"        warn=$((warn + 1))        continue    fi    before=$(git rev-parse HEAD)    if git pull --ff-only --quiet 2>/dev/null; then        after=$(git rev-parse HEAD)        if [ "$before" = "$after" ]; then            printf "  [ok]   %s (up to date)\n" "$name"        else            printf "  [ok]   %s (%s -> %s)\n" "$name" \                "$(git rev-parse --short "$before")" \                "$(git rev-parse --short "$after")"        fi        ok=$((ok + 1))    else        printf "  [warn] %s (not fast-forward; manual pull/rebase needed)\n" "$name"        warn=$((warn + 1))    fidoneecho ""printf "%d updated, %d warned, %d skipped\n" "$ok" "$warn" "$skip"
modified dotfiles/bash_aliases
@@ -1 +1,5 @@# Helper scripts (backup, restore, sync, status) live in ~/scripts and are# invokable as bare commands from anywhere.export PATH="$HOME/scripts:$PATH"alias ls='LC_COLLATE=C ls --color=auto -h --group-directories-first'
added dotfiles/host/ssh-config
@@ -0,0 +1,18 @@# SSH config (host)## Place at: ~\.ssh\config on Windows (i.e. C:\Users\<you>\.ssh\config)## Pairs with the bythewood-webdev container: SSH into the dev environment for# editor remote-dev (Zed, VS Code, JetBrains Gateway). The container's sshd# listens on host port 2222 (mapped from container port 22).Host *    IdentityFile ~/.ssh/home_key    StrictHostKeyChecking accept-newHost bythewood-webdev    HostName localhost    Port 2222    User dev    IdentityFile ~/.ssh/home_key    StrictHostKeyChecking accept-new
added dotfiles/host/zed-settings.json
@@ -0,0 +1,55 @@// Zed settings (host)//// Place at: %APPDATA%\Zed\settings.json on Windows//          (i.e. ~\AppData\Roaming\Zed\settings.json)//// For information on how to configure Zed, see the Zed// documentation: https://zed.dev/docs/configuring-zed//// To see all of Zed's default settings without changing your// custom settings, run `zed: open default settings` from the// command palette (cmd-shift-p / ctrl-shift-p){  "cli_default_open_behavior": "existing_window",  "restore_on_startup": "launchpad",  "expand_excerpt_lines": 5,  "excerpt_context_lines": 2,  "ui_font_family": ".ZedMono",  "ssh_connections": [    {      "host": "bythewood-webdev",      "args": [],      "projects": [        {          "paths": ["/home/dev"]        }      ]    }  ],  "title_bar": {    "show_sign_in": false  },  "git": {    "disable_git": false  },  "disable_ai": true,  "proxy": "",  "show_whitespaces": "boundary",  "telemetry": {    "diagnostics": false,    "metrics": false  },  "agent_servers": {    "claude-acp": {      "type": "registry"    }  },  "base_keymap": "VSCode",  "icon_theme": "Zed (Default)",  "ui_font_size": 14.0,  "buffer_font_size": 14.0,  "theme": "Gruvbox Dark Hard",  "terminal": {    "detect_venv": "off"  }}
modified hosts/alpine/etc/periodic/daily/restic-autobackup
@@ -4,9 +4,12 @@# Initializes the repo on first run. Prunes per the retention policy# (7 daily, 4 weekly, 6 monthly) after each successful backup.## Snapshots are tagged with --host=$RESTIC_HOST for parity with the# webdev backups. Set RESTIC_HOST in /root/.restic/b2-env (e.g. "alpine").## Credentials:#   /root/.restic/password   restic repo password (0600 root)#   /root/.restic/b2-env     exports B2_ACCOUNT_ID and B2_ACCOUNT_KEY (0600 root)#   /root/.restic/b2-env     exports B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_HOST (0600 root)#. /root/.restic/b2-env
@@ -16,6 +19,11 @@ export RESTIC_PASSWORD_FILE="/root/.restic/password"info() { printf "\n%s %s\n\n" "$(date)" "$*" >&2; }trap 'echo $(date) Backup interrupted >&2; exit 2' INT TERMif [ -z "${RESTIC_HOST:-}" ]; then    info "ERROR: RESTIC_HOST is not set. Add 'export RESTIC_HOST=alpine' to /root/.restic/b2-env"    exit 1fiif ! restic cat config >/dev/null 2>&1; then    info "Repository not initialized. Running restic init..."    restic init
@@ -25,6 +33,7 @@ info "Starting backup"restic backup \    --verbose \    --host="$RESTIC_HOST" \    --exclude-caches \    --exclude='node_modules' \    --exclude='.next' \
@@ -44,6 +53,7 @@ backup_exit=$?info "Pruning repository"restic forget --prune \    --host="$RESTIC_HOST" \    --keep-daily   7 \    --keep-weekly  4 \    --keep-monthly 6
modified hosts/alpine/root/restore.sh
@@ -8,12 +8,22 @@# Stops docker before moving /srv (running containers bind-mount /srv/docker/*)# and starts it again after restore completes.## After restore, you'll need to bring projects up manually, e.g.:#     for d in /srv/docker/*; do (cd "$d" && docker compose up --build -d); done# Usage:#   restore.sh           Restore data only; bring up containers manually#   restore.sh --up      Restore data, then bring up every project via#                        `docker compose up --build -d`#set -euUP=0for arg in "$@"; do    case "$arg" in        --up) UP=1 ;;        *) echo "Unknown argument: $arg" >&2; exit 1 ;;    esacdone. /root/.restic/b2-envexport RESTIC_REPOSITORY="b2:overshard-backups:alpine"export RESTIC_PASSWORD_FILE="/root/.restic/password"
@@ -36,12 +46,25 @@ restic restore latest --target /echo "Starting docker..."rc-service docker startif [ "$UP" -eq 1 ]; then    echo ""    echo "Bringing up containers..."    for d in /srv/docker/*; do        if [ -d "$d" ] && [ -f "$d/docker-compose.yml" ]; then            echo "  -> $(basename "$d")"            (cd "$d" && docker compose up --build -d)        fi    donefiecho ""echo "Restore complete. Previous /srv archived at:"echo "  $ARCHIVE/srv"echo ""echo "Bring projects up with:"echo "  for d in /srv/docker/*; do (cd \"\$d\" && docker compose up --build -d); done"echo ""if [ "$UP" -eq 0 ]; then    echo "Bring projects up with:"    echo "  for d in /srv/docker/*; do (cd \"\$d\" && docker compose up --build -d); done"    echo ""fiecho "Once you've verified everything looks right, you can remove the archive:"echo "  rm -rf $ARCHIVE"