feat: package lists per role, with a check-and-install script

The lists are derived rather than typed. beastix has 512 explicit packages -
years of experiments among them - so taking that as "what this machine needs"
would be meaningless. curate.sh combines four usage signals, each of which
alone has a blind spot: shell history sees no GUI application, KDE's activity
database sees no chat client, autostart sees only what starts by itself, and
flatpak entries never appear in pacman -Qqe at all. That reduces 512 to 62.

The split follows the data: what beastix and shron both have becomes the base,
the rest of beastix becomes desktop, shron's own becomes server.

base-devel is one entry in the server list rather than its 26 members. It
stopped being a package group in 2022 - "pacman -Sg base-devel" now fails
outright - and is a meta package today. base, grub and linux-lts are dropped
entirely; they arrive with the operating system.

packages.sh never removes anything. Extras are reported and left alone, for
the same reason the BookStack playbook refuses to: on a server, individual
packages carry mail and web services. It avoids process substitution because
/bin/sh is dash on Debian and a POSIX-mode bash on macOS, and this has to run
on both.

Verified on beastix: 62 wanted, 0 missing, 450 extra reported and untouched;
install is a no-op.

The server list is a snapshot from 2026-08-06 and wants re-reading when shron
is reachable again - its login session has since expired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
thomas.kopp
2026-08-07 16:11:45 +02:00
co-authored by Claude Opus 5
parent d215dfefc5
commit 035c88c31c
8 changed files with 364 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# Build a candidate "daily use" package list from two independent signals.
#
# Shell history alone is blind to anything started from the desktop menu, and a
# .desktop file alone says nothing about whether the thing is ever opened. Taken
# together they cover both halves: CLI tools by how often they were actually
# run, GUI applications by the fact that they were explicitly installed and ship
# a menu entry.
#
# Everything here is a candidate, not a decision. The point is to replace
# guessing about 512 packages with a list short enough to read.
set -u
OUT=/home/templis/.claude/jobs/e44cbeae/tmp
EXPLICIT=$(mktemp)
pacman -Qqe | sort -u > "$EXPLICIT"
# One pass over the file lists instead of one pacman -Qo per command: -Qo forks
# a lookup every time and takes minutes for 40 commands.
MAP="$OUT/file2pkg.txt"
pacman -Ql $(cat "$EXPLICIT") 2>/dev/null | awk '$2 ~ /\/(bin|sbin)\/[^\/]+$/ {n=split($2,a,"/"); print a[n], $1}' | sort -u > "$MAP"
# --- Signal 1: commands actually run, mapped to their package -------------
: > "$OUT/cli_pkgs.txt"
while read -r count cmd; do
[ "${count:-0}" -lt 5 ] && continue
pkg=$(awk -v c="$cmd" '$1 == c {print $2; exit}' "$MAP")
[ -n "$pkg" ] && printf '%s\t%s\t%s\n' "$count" "$pkg" "$cmd"
done < /tmp/claude-1000/cmdfreq.txt | sort -rn -k1 > "$OUT/cli_pkgs.txt"
# --- Signal 2: explicitly installed packages that ship a menu entry -------
: > "$OUT/gui_pkgs.txt"
pacman -Qlq $(cat "$EXPLICIT") 2>/dev/null | grep -E '/usr/share/applications/[^/]+\.desktop$' > "$OUT/desktop_files.txt"
while read -r pkg; do
if pacman -Qlq "$pkg" 2>/dev/null | grep -qE '/usr/share/applications/[^/]+\.desktop$'; then
echo "$pkg"
fi
done < "$EXPLICIT" > "$OUT/gui_pkgs.txt"
echo "CLI-Pakete aus History : $(awk '{print $2}' "$OUT/cli_pkgs.txt" | sort -u | wc -l)"
echo "GUI-Pakete mit Menue : $(wc -l < "$OUT/gui_pkgs.txt")"
rm -f "$EXPLICIT"
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
# Compare the installed packages against the lists for this machine's role.
#
# packages.sh what is missing and what is extra
# packages.sh install install what is missing
#
# It never removes anything. Extra packages are reported and left alone: on a
# server, individual packages carry mail and web services, and a tool that
# tidies up on its own is a tool that eventually takes one of them down. The
# same caution is why the BookStack upgrade playbook refuses to remove
# anything either.
#
# The lists are data, not code - plain names, one per line. That is deliberate:
# they map onto home-manager and environment.systemPackages almost unchanged if
# this ever moves to Nix, whereas an install script would not.
set -eu
ROLE=$(cat "$HOME/.config/dotconfs/role" 2>/dev/null || echo desktop)
DIR="$(cd "$(dirname "$0")/../packages" 2>/dev/null && pwd)" || {
echo "packages/ not found next to $0" >&2
exit 1
}
# Comments and blank lines are for the reader, not the package manager.
read_list() {
[ -f "$1" ] || return 0
sed -E 's/#.*//' "$1" | tr -d '\r' | awk 'NF'
}
case "$(uname -s)" in
Linux)
command -v pacman >/dev/null 2>&1 || { echo "not an Arch machine" >&2; exit 1; }
WANT=$(read_list "$DIR/arch-base.txt"; read_list "$DIR/arch-$ROLE.txt" || true)
HAVE=$(pacman -Qqe)
INSTALL="yay -S --needed --noconfirm"
command -v yay >/dev/null 2>&1 || INSTALL="sudo pacman -S --needed"
;;
Darwin)
command -v brew >/dev/null 2>&1 || { echo "Homebrew not in PATH - is this a login shell?" >&2; exit 1; }
WANT=$(read_list "$DIR/brew-$ROLE.txt")
HAVE=$(brew leaves)
INSTALL="brew install"
;;
*)
echo "unsupported platform: $(uname -s)" >&2
exit 1
;;
esac
# Temporary files rather than process substitution: <(...) is not POSIX, and
# /bin/sh is dash on Debian and a POSIX-mode bash on macOS. This script has to
# run on both.
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT INT TERM
printf '%s\n' "$WANT" | awk 'NF' | sort -u > "$TMP/want"
printf '%s\n' "$HAVE" | awk 'NF' | sort -u > "$TMP/have"
MISSING=$(comm -23 "$TMP/want" "$TMP/have")
EXTRA=$(comm -13 "$TMP/want" "$TMP/have")
WANT=$(cat "$TMP/want")
HAVE=$(cat "$TMP/have")
count() { printf '%s\n' "$1" | awk 'NF' | wc -l | tr -d ' '; }
case "${1:-check}" in
check)
echo "role: $ROLE wanted: $(count "$WANT") installed: $(count "$HAVE")"
echo
echo "missing ($(count "$MISSING")):"
printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /'
echo
echo "extra, not touched ($(count "$EXTRA")):"
printf '%s\n' "$EXTRA" | awk 'NF' | sed 's/^/ /'
;;
install)
if [ -z "$(printf '%s\n' "$MISSING" | awk 'NF')" ]; then
echo "nothing to install"
exit 0
fi
echo "installing:"
printf '%s\n' "$MISSING" | awk 'NF' | sed 's/^/ /'
# shellcheck disable=SC2086
$INSTALL $(printf '%s ' $MISSING)
;;
*)
echo "usage: $0 [check|install]" >&2
exit 1
;;
esac
+27
View File
@@ -0,0 +1,27 @@
# Packages wanted on every Arch machine, regardless of role.
#
# Derived, not curated by hand: the intersection of what beastix actually uses
# (shell history, KDE's activity database, autostart, flatpak - see
# .scripts/curate.sh) with what shron has installed explicitly. A package on
# both machines has earned its place in the base by being useful with and
# without a desktop.
bind
bpytop
docker
ex-vi-compat
git
htop
lnav
man-db
mc
nano
ncdu
neovim
openssh
redis
tailscale
tmux
vim
wget
yay
zsh
+47
View File
@@ -0,0 +1,47 @@
# Packages for the desktop role, on top of arch-base.txt.
#
# From the same four usage signals on beastix, minus everything shron also has.
# The original 512 explicit packages are years of experiments; these are the
# ones something actually points at.
android-tools
blueman
brscan4
claude-code
clicker-git
cwitch
firefox
flameshot
hytale-launcher-bin
inetutils
kdeconnect
kdotool
keepassxc
kitty
libpulse
lsd
mtr-gtk
networkmanager
ntfs-3g
openai-codex
pamixer
path-of-building-community-git
pdftk
pipewire
pipewire-pulse
redshift
seatd
signal-desktop
smartmontools
snapper
steam
sway
tailwindcss-bin
teamspeak
thunderbird
uxplay
whois
wine
wireplumber
yazi
ytdl
zoiper-bin
+78
View File
@@ -0,0 +1,78 @@
# Packages for the server role, on top of arch-base.txt.
#
# Taken from shron's explicit list of 2026-08-06. Re-check it when shron is
# reachable again - the snapshot was read while a login session still existed,
# and that session has since expired.
#
# base-devel appears as a single entry rather than its 26 members. It stopped
# being a package group in 2022 and is a meta package now, so pulling in the
# members individually is both noisier and no longer how it is installed.
# base, grub and linux-lts are left out entirely: they come with the operating
# system and nobody installs them from a dotfiles script.
7zip
acme.sh
base-devel
btop
caddy
certbot
certbot-dns-gandi
certbot-nginx
composer
crowdsec
crowdsec-firewall-bouncer-iptables
docker-compose
dotpac
endlessh-git
fail2ban
ffmpeg
figlet
fzf
gdu
gitmux
kitty-shell-integration
lazydocker
libappimage
libpam-google-authenticator
librsvg
libxcrypt-compat
links
mariadb
mdadm
mtools
mtr
net-tools
nextcloud-client
nginx
nmon
ntfysh-bin
php
php-apcu
php-fpm
php-gd
php-imagick
php-legacy
php-legacy-gd
php-legacy-memcache
php-legacy-memcached
php-legacy-odbc
php-legacy-redis
php-legacy-sodium
php-legacy-sqlite
php-legacy-tidy
php-redis
php-sodium
php-sqlite
php-tidy
powerline
progress
python-pip
python-pipx
rsync
screen
sshfs
teamspeak3-server
unace
unarj
unrar
unzip
zip
+38
View File
@@ -0,0 +1,38 @@
# Homebrew casks for the mobile role - applications rather than command line
# tools, installed with "brew install --cask".
alfred
appcleaner
claude-code
codex
cyberduck
dbeaver-community
drawio
font-hack-nerd-font
font-inconsolata-nerd-font
font-ubuntu-mono-nerd-font
font-ubuntu-nerd-font
ghostty
gimp
iterm2
joplin
jordanbaird-ice
karabiner-elements
keepassxc
kitty
libreoffice
mactex
mullvad-vpn
mullvadvpn
neovide
neovide-app
nextcloud
obsidian
openvpn-connect
rectangle
rustdesk
sublime-text
thunderbird@esr
utm
vimr
vlc
wireshark-chmodbpf
+38
View File
@@ -0,0 +1,38 @@
# Homebrew formulae for the mobile role (the Mac).
#
# "brew leaves" rather than "brew list": leaves are the things asked for, the
# full list is those plus everything dragged in as a dependency.
aria2
bat
cabextract
cdrtools
colima
composer
docker
docker-compose
fd
fzf
git-delta
go
hashcat
helix
imapsync
john
lazygit
midnight-commander
nmap
node
perl
putty
pypy
ripgrep
rust
tailscale
telnet
tmux
tree
vim
wget
wimlib
wireguard-tools
wireshark
+6
View File
@@ -0,0 +1,6 @@
# Flatpaks on the desktop.
#
# These never appear in "pacman -Qqe", which is why they need their own list -
# a package survey based on pacman alone silently loses them.
com.hypixel.HytaleLauncher
org.gnome.Mines