Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
This commit is contained in:
+155
-112
@@ -73,6 +73,148 @@ PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
# shellcheck source=../lib/extras-catalog
|
||||
source "$PANAMA_PATH/setup/lib/extras-catalog"
|
||||
|
||||
# Which machine this is. A server takes the short path below: core tools,
|
||||
# node, the agents -- no third-party repos, no desktop, no flatpaks.
|
||||
# shellcheck source=../lib/machine-role
|
||||
source "$PANAMA_PATH/setup/lib/machine-role"
|
||||
ROLE="$(panama_role)"
|
||||
|
||||
# One list, installed the way every list is installed: --skip-unavailable so a
|
||||
# single rotted name cannot cost the transaction, then report_missing so a
|
||||
# skipped name is a warning somebody reads.
|
||||
install_list() {
|
||||
local file="$PANAMA_PATH/setup/packages/$1" label="$2" packages
|
||||
if [[ -f "$file" ]]; then
|
||||
packages=$(packages_in "$file")
|
||||
log "Installing $label Packages"
|
||||
echo -e "Includes the following packages:"
|
||||
echo -e "$(<"$file")"
|
||||
sudo dnf install -y --skip-unavailable $packages > /dev/null
|
||||
report_missing "$file"
|
||||
log "$label packages installed!"
|
||||
else
|
||||
log "Package list was not in specified path: $file"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Node and pnpm, through nvm ----------------------------------------------
|
||||
#
|
||||
# nvm is a shell function rather than a binary, so it has to be sourced before
|
||||
# it can be used at all -- and its script reads variables that `set -u` above
|
||||
# treats as fatal, so the strictness is lifted for exactly that source and put
|
||||
# straight back.
|
||||
#
|
||||
# Deliberately not dnf's nodejs: config/bash/shell switches Node per project
|
||||
# from .nvmrc, and a system Node earlier on PATH would win every switch, leaving
|
||||
# `nvm use` looking like it did nothing.
|
||||
#
|
||||
# pnpm goes inside the nvm-managed Node rather than beside it as its own dnf
|
||||
# package, so it travels with the version it belongs to instead of outliving it.
|
||||
setup_node() {
|
||||
if [[ -s /etc/profile.d/nvm.sh ]]; then
|
||||
log "Installing the latest Node LTS through nvm"
|
||||
set +u
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/profile.d/nvm.sh
|
||||
if nvm install --lts >/dev/null 2>&1; then
|
||||
nvm alias default 'lts/*' >/dev/null 2>&1 || true
|
||||
npm install -g pnpm >/dev/null 2>&1 || { log "pnpm did not install"; softly_failed+=("pnpm"); }
|
||||
log "Node $(node --version 2>/dev/null) with pnpm $(pnpm --version 2>/dev/null)"
|
||||
else
|
||||
log "nvm could not install Node; skipping"; softly_failed+=("Node (nvm)")
|
||||
fi
|
||||
set -u
|
||||
else
|
||||
log "nvm is not installed, so Node was not set up"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Applications no repository packages -------------------------------------
|
||||
#
|
||||
# Everything else Panama installs comes from dnf or Flathub. These do not
|
||||
# exist in either, so each is an explicit exception with a reason, and each is
|
||||
# skipped when already present so a re-run costs nothing.
|
||||
#
|
||||
# None of them pins a version. sunhat pinned URLs -- upscayl 2.11.5, LACT 0.5.4,
|
||||
# a fedora-40 RPM -- and every one of them was a 404 within a release cycle. An
|
||||
# installer that resolves "latest" keeps working; one that names a version rots.
|
||||
#
|
||||
# A failure here is logged and stepped over rather than aborting: an
|
||||
# unreachable third-party host should not cost the rest of the run.
|
||||
|
||||
# Bun: the JavaScript runtime and package manager. No RPM, no flatpak.
|
||||
install_bun() {
|
||||
if [[ -x "$HOME/.bun/bin/bun" ]]; then
|
||||
log "Bun already installed at \"$HOME/.bun/bin/bun\""
|
||||
else
|
||||
log "Installing Bun via curl..."
|
||||
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 || { log "Bun install failed; skipping"; softly_failed+=("Bun"); }
|
||||
fi
|
||||
}
|
||||
|
||||
# Claude Code: Anthropic's CLI. The official installer keeps itself updated
|
||||
# afterwards, so this runs once and then never needs to again.
|
||||
install_claude_code() {
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
log "Claude Code already installed at \"$(command -v claude)\""
|
||||
else
|
||||
log "Installing Claude Code via the official installer..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash > /dev/null 2>&1 || { log "Claude Code install failed; skipping"; softly_failed+=("Claude Code"); }
|
||||
fi
|
||||
}
|
||||
|
||||
# Codex: OpenAI's CLI. Distributed through npm, which is why this runs after
|
||||
# setup_node -- the nvm-managed Node is the one it should land in.
|
||||
install_codex() {
|
||||
if command -v codex >/dev/null 2>&1; then
|
||||
log "Codex already installed at \"$(command -v codex)\""
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
log "Installing Codex via npm..."
|
||||
npm install -g @openai/codex >/dev/null 2>&1 || { log "Codex install failed; skipping"; softly_failed+=("Codex"); }
|
||||
else
|
||||
log "npm is not available, so Codex was not installed"; softly_failed+=("Codex")
|
||||
fi
|
||||
}
|
||||
|
||||
# --- What was stepped over ---------------------------------------------------
|
||||
#
|
||||
# Tolerating a failure is only better than aborting on it if somebody is told.
|
||||
# The whole point of surviving a soft failure is that the rest gets installed
|
||||
# anyway -- but a machine missing something should say so once, here, rather
|
||||
# than be discovered a week later.
|
||||
report_soft_failures() {
|
||||
if (( ${#softly_failed[@]} > 0 )); then
|
||||
log "Installed, but these were stepped over:"
|
||||
printf ' - %s\n' "${softly_failed[@]}"
|
||||
log "None of them stops the machine, but this run is not recorded as"
|
||||
log "complete, so the next 'panama update' tries them again."
|
||||
# A step that did not complete has not happened. Exiting non-zero is what
|
||||
# keeps ./install from stamping the packages hash over the gaps -- stamped,
|
||||
# they would never be retried (the hash-skip would say nothing changed).
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- The server path ---------------------------------------------------------
|
||||
#
|
||||
# Everything a server runs is above this line plus the lists it installs. No
|
||||
# RPM Fusion, no Terra, no COPR, no multimedia, no flatpaks: those exist for a
|
||||
# desktop, and every one of them is a network dependency and a failure mode a
|
||||
# headless machine has no reason to carry.
|
||||
if [[ "$ROLE" == server ]]; then
|
||||
echo -e "\n--- Installing packages (server) ---"
|
||||
log "Updating all packages. This may take a while"
|
||||
sudo dnf update -y --refresh > /dev/null
|
||||
install_list core-packages "Core"
|
||||
install_list server-packages "Server"
|
||||
setup_node
|
||||
install_bun
|
||||
install_claude_code
|
||||
install_codex
|
||||
report_soft_failures
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "\n--- Installing Repositories ---"
|
||||
log "Installing RPM Fusion Free and Nonfree Repositories"
|
||||
sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm > /dev/null
|
||||
@@ -105,37 +247,14 @@ echo -e "\n--- Installing relevant packages ---"
|
||||
log "Updating all packages. This may take a while"
|
||||
sudo dnf update -y --refresh > /dev/null
|
||||
|
||||
# --- Install all initial packages ---
|
||||
PACKAGES_FILE="$PANAMA_PATH/setup/packages/initial-packages"
|
||||
if [[ -f "$PACKAGES_FILE" ]]; then
|
||||
INITIAL_PACKAGES=$(packages_in "$PACKAGES_FILE")
|
||||
log "Installing Initial Packages"
|
||||
echo -e "Includes the following packages:"
|
||||
echo -e "$(<"$PACKAGES_FILE")"
|
||||
# --skip-unavailable: dnf5 refuses a whole transaction over one missing
|
||||
# name, so a single rotted entry in this list used to cost every package
|
||||
# in it -- and the desktop below never installed. The skipped names are
|
||||
# reported afterwards rather than silently dropped.
|
||||
sudo dnf install -y --skip-unavailable $INITIAL_PACKAGES > /dev/null
|
||||
report_missing "$PACKAGES_FILE"
|
||||
log "Initial packages installed!"
|
||||
else
|
||||
log "Package list was not in specified path: $PACKAGES_FILE"
|
||||
fi
|
||||
|
||||
# --- Install Desktop Packages ---
|
||||
DESKTOP_FILE="$PANAMA_PATH/setup/packages/desktop-packages"
|
||||
if [[ -f "$DESKTOP_FILE" ]]; then
|
||||
DESKTOP_PACKAGES=$(packages_in "$DESKTOP_FILE")
|
||||
log "Installing Desktop Packages"
|
||||
echo -e "Includes the following packages:"
|
||||
echo -e "$(<"$DESKTOP_FILE")"
|
||||
sudo dnf install -y --skip-unavailable $DESKTOP_PACKAGES > /dev/null
|
||||
report_missing "$DESKTOP_FILE"
|
||||
log "Desktop packages installed!"
|
||||
else
|
||||
log "Package list was not in specified path: $DESKTOP_FILE"
|
||||
fi
|
||||
# --- Install the shared core, then the desktop-only lists ---
|
||||
# --skip-unavailable throughout (inside install_list): dnf5 refuses a whole
|
||||
# transaction over one missing name, so a single rotted entry used to cost
|
||||
# every package in a list -- and the desktop below never installed. The
|
||||
# skipped names are reported afterwards rather than silently dropped.
|
||||
install_list core-packages "Core"
|
||||
install_list initial-packages "Initial"
|
||||
install_list desktop-packages "Desktop"
|
||||
|
||||
# --- Install the Hyprland desktop ---
|
||||
#
|
||||
@@ -212,70 +331,10 @@ else
|
||||
log "Package list was not in specified path: $DEV_FILE"
|
||||
fi
|
||||
|
||||
# --- Node and pnpm, through nvm ----------------------------------------------
|
||||
#
|
||||
# nvm is a shell function rather than a binary, so it has to be sourced before
|
||||
# it can be used at all -- and its script reads variables that `set -u` above
|
||||
# treats as fatal, so the strictness is lifted for exactly that source and put
|
||||
# straight back.
|
||||
#
|
||||
# Deliberately not dnf's nodejs: config/bash/shell switches Node per project
|
||||
# from .nvmrc, and a system Node earlier on PATH would win every switch, leaving
|
||||
# `nvm use` looking like it did nothing.
|
||||
#
|
||||
# pnpm goes inside the nvm-managed Node rather than beside it as its own dnf
|
||||
# package, so it travels with the version it belongs to instead of outliving it.
|
||||
if [[ -s /etc/profile.d/nvm.sh ]]; then
|
||||
log "Installing the latest Node LTS through nvm"
|
||||
set +u
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/profile.d/nvm.sh
|
||||
if nvm install --lts >/dev/null 2>&1; then
|
||||
nvm alias default 'lts/*' >/dev/null 2>&1 || true
|
||||
npm install -g pnpm >/dev/null 2>&1 || { log "pnpm did not install"; softly_failed+=("pnpm"); }
|
||||
log "Node $(node --version 2>/dev/null) with pnpm $(pnpm --version 2>/dev/null)"
|
||||
else
|
||||
log "nvm could not install Node; skipping"; softly_failed+=("Node (nvm)")
|
||||
fi
|
||||
set -u
|
||||
else
|
||||
log "nvm is not installed, so Node was not set up"
|
||||
fi
|
||||
|
||||
# --- Applications no repository packages -------------------------------------
|
||||
#
|
||||
# Everything else Panama installs comes from dnf or Flathub. These four do not
|
||||
# exist in either, so each is an explicit exception with a reason, and each is
|
||||
# skipped when already present so a re-run costs nothing.
|
||||
#
|
||||
# Claude Desktop is the half-exception: a repository does carry it, just not one
|
||||
# Fedora or Flathub knows about, so what is exceptional there is adding the
|
||||
# repository rather than installing around one.
|
||||
#
|
||||
# None of them pins a version. sunhat pinned URLs -- upscayl 2.11.5, LACT 0.5.4,
|
||||
# a fedora-40 RPM -- and every one of them was a 404 within a release cycle. An
|
||||
# installer that resolves "latest" keeps working; one that names a version rots.
|
||||
#
|
||||
# A failure here is logged and stepped over rather than aborting: this stage has
|
||||
# already installed the desktop by this point, and an unreachable third-party
|
||||
# host should not cost you that.
|
||||
|
||||
# Bun: the JavaScript runtime and package manager. No RPM, no flatpak.
|
||||
if [[ -x "$HOME/.bun/bin/bun" ]]; then
|
||||
log "Bun already installed at \"$HOME/.bun/bin/bun\""
|
||||
else
|
||||
log "Installing Bun via curl..."
|
||||
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 || { log "Bun install failed; skipping"; softly_failed+=("Bun"); }
|
||||
fi
|
||||
|
||||
# Claude Code: Anthropic's CLI. The official installer keeps itself updated
|
||||
# afterwards, so this runs once and then never needs to again.
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
log "Claude Code already installed at \"$(command -v claude)\""
|
||||
else
|
||||
log "Installing Claude Code via the official installer..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash > /dev/null 2>&1 || { log "Claude Code install failed; skipping"; softly_failed+=("Claude Code"); }
|
||||
fi
|
||||
setup_node
|
||||
install_bun
|
||||
install_claude_code
|
||||
install_codex
|
||||
|
||||
# Claude Desktop: Anthropic ships macOS and Windows only, so this is a community
|
||||
# RPM built from the official release. Panama used to build it from source -- it
|
||||
@@ -406,20 +465,4 @@ for extra in ${PANAMA_EXTRAS:-}; do
|
||||
fi
|
||||
done
|
||||
|
||||
# --- What was stepped over ---------------------------------------------------
|
||||
#
|
||||
# Tolerating a failure is only better than aborting on it if somebody is told.
|
||||
# This stage now survives a codec swap that finds nothing to swap, and the whole
|
||||
# point of surviving it is that the desktop gets installed anyway -- but a
|
||||
# machine missing its video codecs should say so once, here, rather than be
|
||||
# discovered a week later by a video that will not play.
|
||||
if (( ${#softly_failed[@]} > 0 )); then
|
||||
log "Installed, but these were stepped over:"
|
||||
printf ' - %s\n' "${softly_failed[@]}"
|
||||
log "None of them stops the desktop, but this run is not recorded as"
|
||||
log "complete, so the next 'panama update' tries them again."
|
||||
# A step that did not complete has not happened. Exiting non-zero is what
|
||||
# keeps ./install from stamping the packages hash over the gaps -- stamped,
|
||||
# they would never be retried (the hash-skip would say nothing changed).
|
||||
exit 1
|
||||
fi
|
||||
report_soft_failures
|
||||
|
||||
Reference in New Issue
Block a user