diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..484db71 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gnome/.gnome-settings.md5 diff --git a/bitwarden.sh b/bitwarden.sh index 878fc12..3775584 100644 --- a/bitwarden.sh +++ b/bitwarden.sh @@ -1,6 +1,6 @@ #!/bin/bash -export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock" +LINE='export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"' if grep -qF "$LINE" ~/.bashrc; then echo "Bitwarden SSH agent already configured in .bashrc" diff --git a/gnome/apply-gnome-settings.sh b/gnome/apply-gnome-settings.sh new file mode 100755 index 0000000..884eca2 --- /dev/null +++ b/gnome/apply-gnome-settings.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Purpose : Apply gnome-settings.dconf only if it has changed since last applied +# (tracked via an MD5 checksum so user tweaks aren't overwritten every run) + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SRC="$SCRIPT_DIR/gnome-settings.dconf" +CHECKSUM_FILE="$SCRIPT_DIR/.gnome-settings.md5" + +if [ ! -f "$SRC" ]; then + echo "Error: $SRC not found." + exit 1 +fi + +CURRENT_SUM=$(md5sum "$SRC" | awk '{print $1}') +STORED_SUM=$(cat "$CHECKSUM_FILE" 2>/dev/null) + +if [ "$CURRENT_SUM" != "$STORED_SUM" ]; then + echo "GNOME settings have changed, applying..." + dconf load /org/gnome/shell/extensions/ < "$SRC" + echo "$CURRENT_SUM" > "$CHECKSUM_FILE" +else + echo "GNOME settings unchanged, skipping." +fi diff --git a/gnome/gnome-settings.sh b/gnome/gnome-settings.sh index af7650c..3706850 100755 --- a/gnome/gnome-settings.sh +++ b/gnome/gnome-settings.sh @@ -2,6 +2,5 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" gsettings set org.gnome.desktop.interface gtk-theme "Adwaita-dark" -echo "setting wallpaper to ${SCRIPT_DIR}/wallpaper.jpeg" gsettings set org.gnome.desktop.background picture-uri-dark "file://${SCRIPT_DIR}/wallpaper.jpeg" gsettings set org.gnome.desktop.background picture-options 'zoom' diff --git a/install.sh b/install.sh index 1c4e74d..44d7381 100755 --- a/install.sh +++ b/install.sh @@ -105,9 +105,6 @@ install_packages "${MAINTENANCE[@]}" echo "Installing desktop environment..." install_packages "${DESKTOP[@]}" -echo "Installing office packages..." -install_packages "${OFFICE[@]}" - echo "Installing media packages..." install_packages "${MEDIA[@]}" @@ -117,14 +114,7 @@ install_packages "${FONTS[@]}" # Enable all systemd services listed in SERVICES array, then register the SSH # Avahi service so the machine is discoverable over mDNS echo "Configuring services..." -for service in "${SERVICES[@]}"; do - if ! systemctl is-enabled "$service" &> /dev/null; then - echo "Enabling $service..." - sudo systemctl enable "$service" - else - echo "$service is already enabled." - fi -done +enable_services "${SERVICES[@]}" sudo cp /usr/share/doc/avahi/ssh.service /etc/avahi/services/ cd ~/.local/share/coredesktop @@ -178,31 +168,7 @@ if ! systemctl is-active tailscaled &> /dev/null; then fi # Apply GNOME dconf settings only if the settings file has changed since last run -# (tracked via an MD5 checksum to avoid redundant writes) -SRC="$HOME/.local/share/coredesktop/gnome/gnome-settings.dconf" -CHECKSUM_FILE="$HOME/.local/share/coredesktop/gnome/.gnome-settings.md5" - -if [ ! -f "$SRC" ]; then - echo "Error: $SRC not found, exiting." - exit 1 -fi - -CURRENT_SUM=$(md5sum "$SRC" | awk '{print $1}') - -if [ -f "$CHECKSUM_FILE" ]; then - STORED_SUM=$(cat "$CHECKSUM_FILE") -else - STORED_SUM="" -fi - -if [ "$CURRENT_SUM" != "$STORED_SUM" ]; then - echo "GNOME settings have changed, applying..." - dconf load /org/gnome/shell/extensions/ < "$SRC" - echo "$CURRENT_SUM" > "$CHECKSUM_FILE" - echo "Done." -else - echo "GNOME settings unchanged, skipping." -fi +bash ~/.local/share/coredesktop/gnome/apply-gnome-settings.sh # Fix mDNS configuration if systemd-resolved is managing DNS on this system if systemctl is-enabled systemd-resolved.service &> /dev/null; then diff --git a/packages.conf b/packages.conf index 88d2386..094fb79 100644 --- a/packages.conf +++ b/packages.conf @@ -96,6 +96,7 @@ MAINTENANCE=( gnome-disk-utility gnome-extra gnome-keyring + pacman-contrib ) # Desktop environment @@ -134,6 +135,7 @@ SERVICES=( avahi-daemon.service bluetooth.service cups.service + fwupd-refresh.timer gdm.service NetworkManager.service sshd.service diff --git a/skills/sibling-project-updates.md b/skills/sibling-project-updates.md index 3c94381..ade5d42 100644 --- a/skills/sibling-project-updates.md +++ b/skills/sibling-project-updates.md @@ -12,10 +12,12 @@ during its own install step. When a user runs `update`: 1. The coredesktop repo at `~/.local/share/coredesktop` is pulled. -2. All `*.sh` files in `updates.d/` are **sourced** (not executed as subprocesses) +2. All `*.sh` files in `updates.d/` are **sourced**, each in its own subshell, in lexicographic order. -3. Each script runs in the same shell context, with access to `$COREDESKTOP` and - anything sourced before it (e.g. `utils.sh`). +3. Each script can see `$COREDESKTOP`, but nothing defined by earlier hooks — + source `utils.sh` yourself if you need its helpers. A `cd`, `set -e` or `exit` + in one hook can't affect the others. +4. A hook that exits non-zero is listed in the summary at the end of `update`. Because the scripts are sourced from within the cloned repo directory, any file placed there by a sibling project's installer will **survive `git pull`** — git @@ -25,17 +27,28 @@ does not delete untracked files on pull. Existing coredesktop hooks use a numeric prefix to control execution order: -| File | Purpose | -|------------------|---------------------| -| `10-pacman.sh` | Arch system update | -| `20-aur.sh` | AUR packages | -| `30-flatpak.sh` | Flatpak apps | -| `40-ruby.sh` | Ruby/gems | -| `50-bitwarden.sh`| Bitwarden CLI | -| `60-pipx.sh` | pipx packages | +| File | Purpose | +|---------------------------|-----------------------------------------| +| `00-rename-migration.sh` | Legacy command renames | +| `10-pacman.sh` | Arch system update | +| `15-services.sh` | Enable new systemd services | +| `20-aur.sh` | AUR packages | +| `30-flatpak.sh` | Flatpak apps | +| `40-ruby.sh` | mise, Ruby/gems | +| `45-tmux.sh` | TPM and tmux plugins | +| `46-neovim.sh` | Neovim plugins | +| `50-bitwarden.sh` | Bitwarden SSH agent | +| `60-pipx.sh` | pipx packages | +| `65-migrate.sh` | Migrate backup tool | +| `70-claude.sh` | Claude Code setup | +| `80-gnome-extensions.sh` | GNOME extensions | +| `85-gnome-settings.sh` | GNOME extension settings | +| `90-firmware.sh` | Firmware update check | +| `95-cleanup.sh` | Flatpak/pacman cache cleanup | +| `99-reboot-check.sh` | Reboot-needed notice | -Sibling projects should use **70 or higher** to run after all coredesktop -updates complete. +Sibling projects should use an unused number from **71–79** so they run after +the core updates but before coredesktop's end-of-run hooks (80+). ## What to Add to a Sibling Project @@ -68,8 +81,9 @@ run on the machine. ## Available Helpers -Since hooks are sourced in the same shell, they can use any function or variable -already defined by earlier scripts. The most useful are in `utils.sh`: +Hooks don't inherit functions from earlier hooks, so source `utils.sh` for its +helpers. Do this before defining your own `is_installed`, because `utils.sh` has +a pacman-specific function with the same name: ```bash source "$COREDESKTOP/utils.sh" @@ -86,5 +100,5 @@ remove_packages "${MY_REMOVE_PACKAGES[@]}" - [ ] Create `updates.d/7X-.sh` in the sibling repo - [ ] Hook script echoes its name so progress is visible during `update` - [ ] Install step copies the hook to `~/.local/share/coredesktop/updates.d/` -- [ ] Numeric prefix is 70+ (does not conflict with coredesktop's own hooks) +- [ ] Numeric prefix is an unused number in 71–79 - [ ] Script is idempotent — safe to run multiple times without side effects diff --git a/update b/update index b75266b..c2459cb 100755 --- a/update +++ b/update @@ -23,18 +23,28 @@ print_logo COREDESKTOP="$HOME/.local/share/coredesktop" echo "Updating CoreDesktop..." -git -C "$COREDESKTOP" pull +if [[ -z "$UPDATE_REEXECED" ]] && ! git -C "$COREDESKTOP" pull; then + echo "WARNING: could not pull the latest CoreDesktop; running the update hooks already on disk." + export UPDATE_PULL_FAILED=1 +fi mkdir -p "$COREDESKTOP/updates.d" -sudo cp "$COREDESKTOP/update" ~/.local/bin/update -sudo cp "$COREDESKTOP/refresh" ~/.local/bin/refresh +# --remove-destination replaces copies left root-owned by older versions of this script +cp --remove-destination "$COREDESKTOP/update" ~/.local/bin/update +cp --remove-destination "$COREDESKTOP/refresh" ~/.local/bin/refresh if [[ -z "$UPDATE_REEXECED" ]]; then export UPDATE_REEXECED=1 exec ~/.local/bin/update fi +FAILED=() +[[ -n "$UPDATE_PULL_FAILED" ]] && FAILED+=("git pull") + +# Each hook runs in its own subshell so a cd, set -e, exit, or stray variable +# in one hook can't leak into or abort the hooks after it. for plugin in "$COREDESKTOP/updates.d/"*.sh; do - if [ -f "$plugin" ]; then + [ -f "$plugin" ] || continue + ( unset -f is_installed run_update source "$plugin" if declare -f run_update > /dev/null; then @@ -42,7 +52,14 @@ for plugin in "$COREDESKTOP/updates.d/"*.sh; do run_update fi fi - fi + ) + # Checked separately: under `( ... ) ||`, bash would ignore set -e inside the hook + [ $? -eq 0 ] || FAILED+=("$(basename "$plugin")") done +if (( ${#FAILED[@]} > 0 )); then + echo "Done, with errors in: ${FAILED[*]}" + exit 1 +fi + echo "Done." diff --git a/updates.d/00-rename-migration.sh b/updates.d/00-rename-migration.sh index a4fc36b..10e4d0f 100755 --- a/updates.d/00-rename-migration.sh +++ b/updates.d/00-rename-migration.sh @@ -4,12 +4,12 @@ COREDESKTOP="$HOME/.local/share/coredesktop" if [ -f ~/.local/bin/coreupdate ]; then echo "Migrating coreupdate -> update..." - sudo cp "$COREDESKTOP/update" ~/.local/bin/update + cp --remove-destination "$COREDESKTOP/update" ~/.local/bin/update sudo rm -f ~/.local/bin/coreupdate fi if [ -f ~/.local/bin/corerefresh ]; then echo "Migrating corerefresh -> refresh..." - sudo cp "$COREDESKTOP/refresh" ~/.local/bin/refresh + cp --remove-destination "$COREDESKTOP/refresh" ~/.local/bin/refresh sudo rm -f ~/.local/bin/corerefresh fi diff --git a/updates.d/15-services.sh b/updates.d/15-services.sh new file mode 100755 index 0000000..1ce15f9 --- /dev/null +++ b/updates.d/15-services.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COREDESKTOP="$HOME/.local/share/coredesktop" + +source "$COREDESKTOP/utils.sh" +source "$COREDESKTOP/packages.conf" + +echo "Checking for new services..." +enable_services "${SERVICES[@]}" diff --git a/updates.d/40-ruby.sh b/updates.d/40-ruby.sh index d67de20..3b57753 100644 --- a/updates.d/40-ruby.sh +++ b/updates.d/40-ruby.sh @@ -1,5 +1,8 @@ #!/bin/bash +echo "Updating mise..." +mise self-update -y + echo "Updating Ruby and Rails..." mise upgrade --bump ruby mise reshim diff --git a/updates.d/45-tmux.sh b/updates.d/45-tmux.sh new file mode 100755 index 0000000..c0ec4ad --- /dev/null +++ b/updates.d/45-tmux.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +is_installed() { + [ -d "$HOME/.tmux/plugins/tpm" ] +} + +run_update() { + echo "Updating TPM and tmux plugins..." + local failed=0 + for plugin in "$HOME/.tmux/plugins/"*/; do + [ -d "$plugin/.git" ] || continue + echo " $(basename "$plugin")" + git -C "$plugin" pull --ff-only --quiet || failed=1 + done + return $failed +} diff --git a/updates.d/46-neovim.sh b/updates.d/46-neovim.sh new file mode 100755 index 0000000..1081714 --- /dev/null +++ b/updates.d/46-neovim.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Only the plugins are updated; the kickstart.nvim config itself is the user's to edit. +is_installed() { + command -v nvim &>/dev/null && [ -d "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/lazy/lazy.nvim" ] +} + +run_update() { + echo "Updating Neovim plugins..." + nvim --headless "+Lazy! sync" +qa +} diff --git a/updates.d/60-pipx.sh b/updates.d/60-pipx.sh index b6f0366..ef5602a 100644 --- a/updates.d/60-pipx.sh +++ b/updates.d/60-pipx.sh @@ -1,4 +1,18 @@ #!/bin/bash -echo "Updating pipx applications..." -pipx upgrade-all +# pipx venvs are tied to the Python they were built with, so a Python upgrade +# breaks them in a way `pipx upgrade-all` can't repair. Rebuild them when the +# Python version changes, or when an upgrade fails. +STAMP="$HOME/.local/state/coredesktop/pipx-python-version" +CURRENT_PYTHON="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" + +if [ "$(cat "$STAMP" 2>/dev/null)" != "$CURRENT_PYTHON" ]; then + echo "Python changed to $CURRENT_PYTHON, reinstalling pipx applications..." + pipx reinstall-all +else + echo "Updating pipx applications..." + if ! pipx upgrade-all; then + echo "pipx upgrade-all failed, reinstalling pipx applications..." + pipx reinstall-all + fi +fi && mkdir -p "$(dirname "$STAMP")" && echo "$CURRENT_PYTHON" > "$STAMP" diff --git a/updates.d/65-migrate.sh b/updates.d/65-migrate.sh new file mode 100755 index 0000000..363a79c --- /dev/null +++ b/updates.d/65-migrate.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +is_installed() { + [ -f "$HOME/.local/bin/migrate" ] +} + +run_update() { + echo "Updating Migrate..." + local tmp + tmp="$(mktemp)" + if curl -fsSL -o "$tmp" https://github.com/CyphrRiot/Migrate/raw/main/bin/migrate && [ -s "$tmp" ]; then + chmod +x "$tmp" + mv "$tmp" "$HOME/.local/bin/migrate" + else + rm -f "$tmp" + echo "Could not download Migrate; keeping the current version." + return 1 + fi +} diff --git a/updates.d/80-gnome-extensions.sh b/updates.d/80-gnome-extensions.sh new file mode 100755 index 0000000..0a72a89 --- /dev/null +++ b/updates.d/80-gnome-extensions.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +is_installed() { + command -v gnome-shell &>/dev/null +} + +run_update() { + echo "Updating GNOME extensions..." + + # Reinstall gext if missing or broken (e.g. its pipx venv went stale after a Python upgrade) + if ! ~/.local/bin/gext --version &> /dev/null; then + echo "gext missing or broken, reinstalling..." + pipx uninstall gnome-extensions-cli 2>/dev/null || true + pipx install --python "$(which python3)" gnome-extensions-cli --system-site-packages + fi + + # Update every enabled user extension to the latest version for this GNOME Shell. + # The filesystem backend writes directly to ~/.local/share/gnome-shell/extensions; + # GNOME Shell picks up the new code at next login. + if ~/.local/bin/gext --filesystem update --user --yes; then + echo "GNOME extensions up to date. Log out and back in to load any updated extensions." + else + echo "gext update failed; GNOME extensions were not updated." + fi +} diff --git a/updates.d/85-gnome-settings.sh b/updates.d/85-gnome-settings.sh new file mode 100755 index 0000000..f7b6d36 --- /dev/null +++ b/updates.d/85-gnome-settings.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +COREDESKTOP="$HOME/.local/share/coredesktop" + +is_installed() { + command -v dconf &>/dev/null +} + +run_update() { + bash "$COREDESKTOP/gnome/apply-gnome-settings.sh" +} diff --git a/updates.d/90-firmware.sh b/updates.d/90-firmware.sh new file mode 100755 index 0000000..db80d60 --- /dev/null +++ b/updates.d/90-firmware.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Firmware updates often need a reboot, so this only reports what's available. +is_installed() { + command -v fwupdmgr &>/dev/null +} + +run_update() { + echo "Checking for firmware updates..." + fwupdmgr refresh &>/dev/null + # get-updates exits 2 when there is nothing to update + if fwupdmgr get-updates --no-unreported-check 2>/dev/null; then + echo "Firmware updates are available. Run 'fwupdmgr update' to install them." + else + echo "No firmware updates available." + fi + return 0 +} diff --git a/updates.d/95-cleanup.sh b/updates.d/95-cleanup.sh new file mode 100755 index 0000000..fead088 --- /dev/null +++ b/updates.d/95-cleanup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Removing unused Flatpak runtimes..." +flatpak uninstall --unused -y --noninteractive + +if command -v paccache &>/dev/null; then + echo "Trimming pacman package cache..." + sudo paccache -rk2 + sudo paccache -ruk0 +fi diff --git a/updates.d/99-reboot-check.sh b/updates.d/99-reboot-check.sh new file mode 100755 index 0000000..3068171 --- /dev/null +++ b/updates.d/99-reboot-check.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# If the running kernel's modules are gone, a newer kernel was installed +if [ ! -d "/usr/lib/modules/$(uname -r)" ]; then + echo "A newer kernel has been installed. Please reboot when convenient." +fi diff --git a/utils.sh b/utils.sh index f85d68c..ee0472e 100755 --- a/utils.sh +++ b/utils.sh @@ -59,4 +59,16 @@ remove_flatpaks() { echo "Removing flatpaks: ${to_remove[*]}" flatpak uninstall --noninteractive "${to_remove[@]}" fi -} \ No newline at end of file +} + +# Function to enable systemd services if not already enabled +enable_services() { + for service in "$@"; do + if ! systemctl is-enabled "$service" &> /dev/null; then + echo "Enabling $service..." + sudo systemctl enable "$service" + else + echo "$service is already enabled." + fi + done +}