Files
platform/scripts/setup.sh
T
2026-03-04 18:11:33 +00:00

829 lines
26 KiB
Bash
Executable File

#!/bin/bash
# Officer — full host dependency setup
# Run once on a fresh Ubuntu/Debian host before launching the server.
# Usage: bash scripts/setup.sh
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
warn() { echo -e " ${YELLOW}!${NC} $1"; }
fail() { echo -e " ${RED}${NC} $1"; }
skip() { echo -e " - $1 (already installed)"; }
has() { command -v "$1" &>/dev/null; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# ─── detect package manager ────────────────────────────────────────────────────
if has apt; then
PM=apt
elif has pacman; then
PM=pacman
elif has brew; then
PM=brew
else
fail "No supported package manager found (apt, pacman, brew)"
exit 1
fi
install_pkg() {
case $PM in
apt) sudo apt install -y "$@" ;;
pacman) sudo pacman -S --noconfirm "$@" ;;
brew) brew install "$@" ;;
esac
}
echo ""
echo "═══════════════════════════════════════════"
echo " Officer — dependency setup ($PM)"
echo "═══════════════════════════════════════════"
# ─── 1. core system packages ───────────────────────────────────────────────────
echo ""
echo "── Core system packages ──"
CORE_PKGS=()
# git
if has git; then skip "git"; else CORE_PKGS+=(git); fi
# zip / unzip
if has zip; then skip "zip"; else CORE_PKGS+=(zip); fi
if has unzip; then skip "unzip"; else CORE_PKGS+=(unzip); fi
# curl / wget
if has curl; then skip "curl"; else CORE_PKGS+=(curl); fi
if has wget; then skip "wget"; else CORE_PKGS+=(wget); fi
# zsh
if has zsh; then skip "zsh"; else CORE_PKGS+=(zsh); fi
# psmisc (fuser) and procps (pgrep)
if has fuser; then skip "fuser (psmisc)"; else
case $PM in
apt|pacman) CORE_PKGS+=(psmisc) ;;
brew) skip "fuser (not needed on macOS)" ;;
esac
fi
if has pgrep; then skip "pgrep (procps)"; else
case $PM in
apt) CORE_PKGS+=(procps) ;;
pacman) CORE_PKGS+=(procps-ng) ;;
brew) skip "pgrep (built-in on macOS)" ;;
esac
fi
# script (bsdutils on apt, util-linux on pacman, built-in on macOS)
if has script; then skip "script (bsdutils)"; else
case $PM in
apt) CORE_PKGS+=(bsdutils) ;;
pacman) CORE_PKGS+=(util-linux) ;;
brew) skip "script (built-in on macOS)" ;;
esac
fi
# build tools (make, gcc, g++) — needed for native npm modules like node-pty
if has make && has gcc; then skip "build tools (make, gcc, g++)"; else
case $PM in
apt) CORE_PKGS+=(build-essential) ;;
pacman) CORE_PKGS+=(base-devel) ;;
brew) warn "Install Xcode command line tools: xcode-select --install" ;;
esac
fi
# pkg-config — needed by cgo-based Go packages (e.g. ebitengine/oto for cliamp)
if has pkg-config; then skip "pkg-config"; else
case $PM in
apt) CORE_PKGS+=(pkg-config) ;;
pacman) CORE_PKGS+=(pkgconf) ;;
brew) CORE_PKGS+=(pkg-config) ;;
esac
fi
# python3 + pip + venv
if has python3; then skip "python3"; else
case $PM in
apt) CORE_PKGS+=(python3 python3-pip python3-venv) ;;
pacman) CORE_PKGS+=(python python-pip) ;;
brew) CORE_PKGS+=(python3) ;;
esac
fi
# ensure pip/venv even if python3 already exists (apt splits them)
if has python3 && [ "$PM" = "apt" ]; then
if ! dpkg -s python3-pip &>/dev/null 2>&1; then CORE_PKGS+=(python3-pip); fi
if ! dpkg -s python3-venv &>/dev/null 2>&1; then CORE_PKGS+=(python3-venv); fi
fi
# shell utilities
for tool in tree btop tmux jq htop lsof duf; do
if has "$tool"; then skip "$tool"; else CORE_PKGS+=("$tool"); fi
done
# sqlite3
if has sqlite3; then skip "sqlite3"; else
case $PM in
apt) CORE_PKGS+=(sqlite3) ;;
pacman) CORE_PKGS+=(sqlite) ;;
brew) CORE_PKGS+=(sqlite) ;;
esac
fi
# ripgrep
if has rg; then skip "ripgrep"; else
case $PM in
apt) CORE_PKGS+=(ripgrep) ;;
pacman) CORE_PKGS+=(ripgrep) ;;
brew) CORE_PKGS+=(ripgrep) ;;
esac
fi
# fd-find
if has fd || has fdfind; then skip "fd-find"; else
case $PM in
apt) CORE_PKGS+=(fd-find) ;;
pacman) CORE_PKGS+=(fd) ;;
brew) CORE_PKGS+=(fd) ;;
esac
fi
# net-tools, less, file, man-db
case $PM in
apt)
for pkg in net-tools less file man-db; do
if dpkg -s "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else CORE_PKGS+=("$pkg"); fi
done
;;
pacman)
for pkg in net-tools less file man-db; do
if pacman -Qi "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else CORE_PKGS+=("$pkg"); fi
done
;;
brew)
skip "net-tools, less, file, man (built-in on macOS)"
;;
esac
# locales
case $PM in
apt)
if dpkg -s locales &>/dev/null 2>&1; then skip "locales"; else CORE_PKGS+=(locales); fi
;;
esac
# ca-certificates
case $PM in
apt)
if dpkg -s ca-certificates &>/dev/null 2>&1; then skip "ca-certificates"; else CORE_PKGS+=(ca-certificates); fi
;;
esac
if [ ${#CORE_PKGS[@]} -gt 0 ]; then
install_pkg "${CORE_PKGS[@]}"
ok "Installed: ${CORE_PKGS[*]}"
fi
# locale generation (ensure en_US.UTF-8)
case $PM in
apt)
if ! locale -a 2>/dev/null | grep -q "en_US.utf8"; then
sudo sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen
sudo locale-gen
ok "Generated en_US.UTF-8 locale"
else
skip "en_US.UTF-8 locale"
fi
;;
esac
# symlink fdfind → fd (apt installs as fdfind)
if has fdfind && ! has fd; then
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
ok "Symlinked fdfind → fd"
fi
# ─── 2. archive extras (optional but useful) ──────────────────────────────────
echo ""
echo "── Archive utilities (optional) ──"
ARCHIVE_PKGS=()
# p7zip
if has 7z; then skip "7z (p7zip)"; else
case $PM in
apt) ARCHIVE_PKGS+=(p7zip-full) ;;
pacman) ARCHIVE_PKGS+=(p7zip) ;;
brew) ARCHIVE_PKGS+=(p7zip) ;;
esac
fi
# unrar
if has unrar; then skip "unrar"; else
case $PM in
apt) ARCHIVE_PKGS+=(unrar) ;;
pacman) ARCHIVE_PKGS+=(unrar) ;;
brew) ARCHIVE_PKGS+=(unrar) ;;
esac
fi
if [ ${#ARCHIVE_PKGS[@]} -gt 0 ]; then
install_pkg "${ARCHIVE_PKGS[@]}" || warn "Some archive packages may need non-free repos"
ok "Installed: ${ARCHIVE_PKGS[*]}"
fi
# ─── 3. ffmpeg ─────────────────────────────────────────────────────────────────
echo ""
echo "── FFmpeg ──"
if has ffmpeg; then
skip "ffmpeg ($(ffmpeg -version 2>&1 | head -1 | awk '{print $3}'))"
else
install_pkg ffmpeg
ok "ffmpeg installed"
fi
# ─── 4. sudoers for officer service user ──────────────────────────────────────
echo ""
echo "── Sudoers (Linux user isolation) ──"
SERVICE_USER="$(whoami)"
SUDOERS_FILE="/etc/sudoers.d/officer-service"
if [ -f "$SUDOERS_FILE" ] && grep -q "$SERVICE_USER" "$SUDOERS_FILE" 2>/dev/null; then
skip "sudoers entry for $SERVICE_USER"
else
case $PM in
apt|pacman)
echo "$SERVICE_USER ALL=(ALL) NOPASSWD: ALL" | sudo tee "$SUDOERS_FILE" > /dev/null
sudo chmod 0440 "$SUDOERS_FILE"
ok "Created sudoers entry for $SERVICE_USER at $SUDOERS_FILE"
;;
brew)
warn "Sudoers setup is Linux-only — skipping on macOS"
;;
esac
fi
# ─── 4b. Disable auto-suspend (server doesn't need to sleep) ──────────────────
echo ""
echo "── Auto-suspend (disable for server) ──"
# Disable system suspend/hibernate
if systemctl is-enabled sleep.target 2>/dev/null | grep -q "enabled\|masked"; then
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
ok "Masked sleep/suspend targets"
else
skip "sleep targets already masked"
fi
# Configure logind to ignore idle — patch individual keys, don't overwrite the file
set_logind_key() {
local key="$1" val="$2" file="/etc/systemd/logind.conf"
if grep -qE "^${key}=" "$file" 2>/dev/null; then
sudo sed -i "s|^${key}=.*|${key}=${val}|" "$file"
elif grep -qE "^#${key}=" "$file" 2>/dev/null; then
sudo sed -i "s|^#${key}=.*|${key}=${val}|" "$file"
else
echo "${key}=${val}" | sudo tee -a "$file" > /dev/null
fi
}
set_logind_key HandleLidSwitch ignore
set_logind_key HandleLidSwitchExternalPower ignore
set_logind_key HandlePowerKey ignore
set_logind_key IdleAction none
set_logind_key RuntimeDirectorySize 10%
sudo systemctl restart systemd-logind
ok "Configured logind to disable auto-suspend"
# ─── 5. Node.js 22 (system-wide) ─────────────────────────────────────────────
echo ""
echo "── Node.js 22 (system-wide) ──"
# Always check the canonical system path, not `which node` (which may resolve nvm).
SYSTEM_NODE="/usr/bin/node"
install_node22_apt() {
echo " Setting up NodeSource repository..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
echo " Installing nodejs..."
sudo apt-get install -y nodejs
}
case $PM in
apt)
NEED_INSTALL=0
if [ -f "$SYSTEM_NODE" ]; then
SYS_MAJOR=$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v')
if [ "$SYS_MAJOR" = "22" ]; then
skip "node v$("$SYSTEM_NODE" -v) (system-wide at $SYSTEM_NODE)"
else
warn "System node v$("$SYSTEM_NODE" -v) at $SYSTEM_NODE is not v22 — upgrading..."
NEED_INSTALL=1
fi
else
if has node; then
warn "node found at $(which node) (not system-wide, likely nvm) — installing Node 22 system-wide..."
else
echo " Node.js not found — installing v22..."
fi
NEED_INSTALL=1
fi
if [ "$NEED_INSTALL" = "1" ]; then
install_node22_apt
if [ -f "$SYSTEM_NODE" ] && [ "$("$SYSTEM_NODE" -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
ok "node v$("$SYSTEM_NODE" -v) installed at $SYSTEM_NODE"
if has node && [ "$(which node)" != "$SYSTEM_NODE" ]; then
warn "Shell resolves 'node' to $(which node) — system node is at $SYSTEM_NODE"
warn "nvm may shadow it in interactive shells; systemd services will use $SYSTEM_NODE"
fi
else
fail "Node.js 22 install failed — $SYSTEM_NODE not found or wrong version"
exit 1
fi
fi
;;
pacman)
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
skip "node v$(node -v)"
else
install_pkg nodejs npm
if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi
fi
;;
brew)
if has node && [ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d 'v')" = "22" ]; then
skip "node v$(node -v)"
else
install_pkg node
if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi
fi
;;
esac
# ─── 6. Bun ───────────────────────────────────────────────────────────────────
echo ""
echo "── Bun ──"
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
if [ -f "$BUN_INSTALL/bin/bun" ]; then
skip "bun ($(bun --version 2>/dev/null))"
else
curl -fsSL https://bun.sh/install | bash
if [ ! -f "$BUN_INSTALL/bin/bun" ]; then fail "bun install failed"; exit 1; fi
ok "bun $(bun --version) installed"
fi
# Symlink to system-wide path so all users and systemd services can access it
if [ ! -L /usr/local/bin/bun ] || [ "$(readlink /usr/local/bin/bun)" != "$BUN_INSTALL/bin/bun" ]; then
sudo ln -sf "$BUN_INSTALL/bin/bun" /usr/local/bin/bun
ok "bun symlinked to /usr/local/bin/bun"
else
skip "bun symlink at /usr/local/bin/bun"
fi
# ─── 7. Go ─────────────────────────────────────────────────────────────────────
echo ""
echo "── Go ──"
echo " Fetching latest Go version..."
GOLANG_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | tr -d 'go')
if [ -z "$GOLANG_VERSION" ]; then
warn "Could not fetch latest Go version — falling back to 1.23.6"
GOLANG_VERSION=1.23.6
fi
echo " Latest Go: $GOLANG_VERSION"
install_go() {
case $PM in
apt|pacman)
ARCH=$(uname -m)
case $ARCH in
x86_64) GO_ARCH=amd64 ;;
aarch64) GO_ARCH=arm64 ;;
*) GO_ARCH=amd64 ;;
esac
echo " Installing Go ${GOLANG_VERSION} from official tarball..."
curl -fsSL "https://go.dev/dl/go${GOLANG_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
export PATH="/usr/local/go/bin:$PATH"
;;
brew)
brew install go
;;
esac
}
if has go; then
INSTALLED_GO=$(go version 2>/dev/null | awk '{print $3}' | tr -d 'go')
if [ "$INSTALLED_GO" = "$GOLANG_VERSION" ]; then
skip "go $INSTALLED_GO"
else
warn "go $INSTALLED_GO installed but latest is $GOLANG_VERSION — upgrading..."
install_go
if has go; then ok "go $(go version | awk '{print $3}') installed"; else warn "go upgrade failed"; fi
fi
else
install_go
if has go; then ok "go $(go version | awk '{print $3}') installed"; else warn "go not found — install manually from https://go.dev/dl/"; fi
fi
# ─── 8. Rust ──────────────────────────────────────────────────────────────────
echo ""
echo "── Rust ──"
if has rustc && has cargo; then
skip "rust ($(rustc --version 2>/dev/null | awk '{print $2}'))"
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
export PATH="$HOME/.cargo/bin:$PATH"
if has rustc; then ok "rust installed"; else warn "rust install failed"; fi
fi
# ─── 9. PulseAudio (headless audio for cliamp) ────────────────────────────────
echo ""
echo "── PulseAudio (headless audio) ──"
PULSE_PKGS=()
if has pulseaudio; then skip "pulseaudio"; else
case $PM in
apt) PULSE_PKGS+=(pulseaudio) ;;
pacman) PULSE_PKGS+=(pulseaudio) ;;
brew) warn "PulseAudio: brew install pulseaudio (cliamp audio won't work without it)" ;;
esac
fi
# pulseaudio-utils provides parec and pactl
if has parec && has pactl; then skip "pulseaudio-utils (parec, pactl)"; else
case $PM in
apt) PULSE_PKGS+=(pulseaudio-utils) ;;
pacman) ;; # included in pulseaudio package
brew) ;; # included in pulseaudio formula
esac
fi
# ALSA dev headers (needed to compile cliamp's Go audio library)
case $PM in
apt)
if dpkg -s libasound2-dev &>/dev/null 2>&1; then skip "libasound2-dev"; else PULSE_PKGS+=(libasound2-dev); fi
;;
pacman)
if pacman -Qi alsa-lib &>/dev/null 2>&1; then skip "alsa-lib"; else PULSE_PKGS+=(alsa-lib); fi
;;
brew) ;; # not needed on macOS
esac
if [ ${#PULSE_PKGS[@]} -gt 0 ]; then
install_pkg "${PULSE_PKGS[@]}"
ok "Installed: ${PULSE_PKGS[*]}"
fi
# ─── 10. cliamp (music player) ────────────────────────────────────────────────
echo ""
echo "── cliamp ──"
GOPATH_BIN="${GOPATH:-$HOME/go}/bin"
export PATH="$GOPATH_BIN:$PATH"
if has cliamp; then
skip "cliamp ($(which cliamp))"
else
if ! has go; then
warn "Go not installed — skipping cliamp build"
else
echo " Building cliamp from source..."
TMPDIR=$(mktemp -d)
git clone --depth=1 https://github.com/bjarneo/cliamp.git "$TMPDIR/cliamp"
(cd "$TMPDIR/cliamp" && go install .)
rm -rf "$TMPDIR"
if [ -f "$GOPATH_BIN/cliamp" ]; then
ok "cliamp installed at $GOPATH_BIN/cliamp"
else
fail "cliamp build failed"
fi
fi
fi
# ─── 11. Neovim ──────────────────────────────────────────────────────────────
echo ""
echo "── Neovim ──"
if has nvim; then
skip "neovim ($(nvim --version 2>/dev/null | head -1))"
else
case $PM in
apt)
echo " Installing Neovim from GitHub releases..."
ARCH=$(uname -m)
case $ARCH in
x86_64) NVIM_ARCH=x86_64 ;;
aarch64) NVIM_ARCH=aarch64 ;;
*) NVIM_ARCH=x86_64 ;;
esac
curl -fsSL "https://github.com/neovim/neovim/releases/latest/download/nvim-linux-${NVIM_ARCH}.tar.gz" -o /tmp/nvim.tar.gz
sudo tar -C /opt -xzf /tmp/nvim.tar.gz
sudo ln -sf "/opt/nvim-linux-${NVIM_ARCH}/bin/nvim" /usr/local/bin/nvim
rm /tmp/nvim.tar.gz
;;
pacman) install_pkg neovim ;;
brew) install_pkg neovim ;;
esac
if has nvim; then ok "neovim installed"; else warn "neovim install failed"; fi
fi
# LazyVim starter config
if [ -d "$HOME/.config/nvim" ]; then
skip "nvim config (already exists at ~/.config/nvim)"
else
echo " Installing LazyVim starter config..."
git clone --depth 1 https://github.com/LazyVim/starter "$HOME/.config/nvim"
rm -rf "$HOME/.config/nvim/.git"
ok "LazyVim starter installed at ~/.config/nvim"
fi
# ─── 12. Terminal tools ──────────────────────────────────────────────────────
echo ""
echo "── Terminal tools (starship, oh-my-zsh, eza, lazygit) ──"
# Starship prompt
if has starship; then
skip "starship"
else
curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin
if has starship; then ok "starship installed"; else warn "starship install failed"; fi
fi
# Deploy starship config
mkdir -p "$HOME/.config"
cp "$SCRIPT_DIR/starship.toml" "$HOME/.config/starship.toml"
ok "starship config deployed"
# Oh-My-Zsh
if [ -d "$HOME/.oh-my-zsh" ]; then
skip "oh-my-zsh (already at ~/.oh-my-zsh)"
else
git clone --depth 1 https://github.com/ohmyzsh/ohmyzsh.git "$HOME/.oh-my-zsh"
ok "oh-my-zsh installed at ~/.oh-my-zsh"
fi
# eza
if has eza; then
skip "eza"
else
case $PM in
apt)
echo " Fetching latest eza version..."
EZA_VERSION=$(curl -fsSL "https://api.github.com/repos/eza-community/eza/releases/latest" | jq -r '.tag_name' | tr -d 'v')
if [ -z "$EZA_VERSION" ]; then warn "Could not fetch eza version — skipping"; else
ARCH=$(uname -m)
case $ARCH in
x86_64) EZA_ARCH=x86_64 ;;
aarch64) EZA_ARCH=aarch64 ;;
*) EZA_ARCH=x86_64 ;;
esac
curl -fsSL "https://github.com/eza-community/eza/releases/download/v${EZA_VERSION}/eza_${EZA_ARCH}-unknown-linux-gnu.tar.gz" -o /tmp/eza.tar.gz
tar -xzf /tmp/eza.tar.gz -C /tmp
sudo mv /tmp/eza /usr/local/bin/eza
sudo chmod +x /usr/local/bin/eza
rm -f /tmp/eza.tar.gz
fi
;;
pacman) install_pkg eza ;;
brew) install_pkg eza ;;
esac
if has eza; then ok "eza installed"; else warn "eza install failed"; fi
fi
# lazygit
if has lazygit; then
skip "lazygit"
else
case $PM in
apt)
echo " Fetching latest lazygit version..."
LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | tr -d 'v')
if [ -z "$LAZYGIT_VERSION" ]; then warn "Could not fetch lazygit version — skipping"; else
ARCH=$(uname -m)
case $ARCH in
x86_64) LG_ARCH=x86_64 ;;
aarch64) LG_ARCH=arm64 ;;
*) LG_ARCH=x86_64 ;;
esac
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_${LG_ARCH}.tar.gz" -o /tmp/lazygit.tar.gz
tar -xzf /tmp/lazygit.tar.gz -C /tmp
sudo mv /tmp/lazygit /usr/local/bin/lazygit
sudo chmod +x /usr/local/bin/lazygit
rm -f /tmp/lazygit.tar.gz /tmp/LICENSE /tmp/README.md
fi
;;
pacman) install_pkg lazygit ;;
brew) install_pkg lazygit ;;
esac
if has lazygit; then ok "lazygit installed"; else warn "lazygit install failed"; fi
fi
# ─── 13. yt-dlp (optional — video/audio download) ────────────────────────────
echo ""
echo "── yt-dlp (optional) ──"
if has yt-dlp; then
skip "yt-dlp"
else
case $PM in
apt) install_pkg yt-dlp 2>/dev/null && ok "yt-dlp installed" || warn "yt-dlp not in apt repos — install via pip or GitHub" ;;
pacman) install_pkg yt-dlp && ok "yt-dlp installed" ;;
brew) install_pkg yt-dlp && ok "yt-dlp installed" ;;
esac
fi
# ─── 14. npm global packages (system-wide) ──────────────────────────────────
echo ""
echo "── npm global packages (system-wide) ──"
if ! has npm; then
warn "npm not found — skipping global package installs"
else
# Use explicit path to system npm if available, to avoid nvm conflicts
SYSTEM_NPM="/usr/bin/npm"
if [ ! -f "$SYSTEM_NPM" ]; then
SYSTEM_NPM="$(which npm)"
fi
# Set npm prefix to system location so all users can access packages
echo " Configuring npm global prefix to /usr/local..."
sudo "$SYSTEM_NPM" config set prefix /usr/local
ok "npm prefix set to /usr/local"
# Update npm itself to latest
echo " Updating npm to latest..."
sudo "$SYSTEM_NPM" install -g npm@latest
ok "npm updated to v$(npm --version 2>/dev/null)"
# Pi (coding agent)
if has pi; then
skip "pi (@mariozechner/pi-coding-agent)"
else
echo " Installing pi..."
sudo "$SYSTEM_NPM" install -g @mariozechner/pi-coding-agent
if has pi; then ok "pi installed"; else warn "pi install failed"; fi
fi
# Validate Pi works
if has pi; then
if pi --list-models > /dev/null 2>&1; then
ok "pi validated (--list-models works)"
else
warn "pi installed but --list-models failed — check API keys in ~/.pi/agent/auth.json"
fi
fi
# Fix ~/.pi ownership if needed (in case of mixed user/sudo installs)
if [ -d "$HOME/.pi" ]; then
if find "$HOME/.pi" -not -user "$USER" -print -quit 2>/dev/null | grep -q .; then
sudo chown -R "$USER:$(id -gn)" "$HOME/.pi"
ok "Fixed ~/.pi ownership to $USER"
fi
fi
# Claude Code
if has claude; then
skip "claude (@anthropic-ai/claude-code)"
else
echo " Installing claude-code..."
sudo "$SYSTEM_NPM" install -g @anthropic-ai/claude-code
if has claude; then ok "claude-code installed"; else warn "claude-code install failed"; fi
fi
# pm2 (process manager)
if has pm2; then
skip "pm2"
else
echo " Installing pm2..."
sudo "$SYSTEM_NPM" install -g pm2
if has pm2; then ok "pm2 installed"; else warn "pm2 install failed"; fi
fi
fi
# ─── 15. bun install (project dependencies) ──────────────────────────────────
echo ""
echo "── Project dependencies ──"
if has bun && [ -f "$PROJECT_DIR/package.json" ]; then
echo " Running bun install..."
(cd "$PROJECT_DIR" && bun install)
ok "Project dependencies installed"
else
warn "Skipping bun install (bun not found or not in project dir)"
fi
# ─── 16. remote desktop (XFCE + VNC) ────────────────────────────────────────
echo ""
echo "── Remote Desktop (XFCE + VNC) ──"
if systemctl is-active --quiet officer-vnc 2>/dev/null; then
skip "remote desktop (officer-vnc service already running)"
else
case $PM in
apt)
bash "$SCRIPT_DIR/setup-desktop.sh"
;;
*)
warn "Remote desktop setup is Ubuntu/Debian only — skipping"
;;
esac
fi
# ─── 17. PTY sidecar (systemd service) ──────────────────────────────────────
bash "$SCRIPT_DIR/setup-pty-sidecar.sh"
# ─── verification ─────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
echo " Verification"
echo "═══════════════════════════════════════════"
echo ""
check() {
if has "$1"; then ok "$1"; else fail "$1 — NOT FOUND"; fi
}
echo "Required:"
check git
check node
check bun
check npm
check ffmpeg
check zip
check script
check python3
check make
check gcc
echo ""
echo "Dev tools:"
check go
check rustc
check cargo
check nvim
check zsh
check starship
check lazygit
check eza
check rg
check fd
check jq
check htop
check tmux
check tree
check btop
check sqlite3
echo ""
echo "Audio (cliamp):"
check pulseaudio
check parec
check pactl
check cliamp
echo ""
echo "AI agents:"
check pi
check claude
echo ""
echo "Process manager:"
check pm2
echo ""
echo "Optional:"
check unzip
check 7z
check unrar
check pgrep
check fuser
check yt-dlp
echo ""
echo "═══════════════════════════════════════════"
echo " Setup complete!"
echo "═══════════════════════════════════════════"
echo ""
echo "Notes:"
echo " • PulseAudio null sink starts automatically with the server"
echo " • Make sure ~/go/bin is in your PATH for cliamp"
echo " • Make sure ~/.cargo/bin is in your PATH for Rust tools"
echo " • sharp, whisper-cpp, mlx-audio can be installed from Settings > Applications"
echo ""