#!/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 # isync (provides mbsync for Gmail IMAP sync) if has mbsync; then skip "isync (mbsync)"; else case $PM in apt) CORE_PKGS+=(isync) ;; pacman) CORE_PKGS+=(isync) ;; brew) CORE_PKGS+=(isync) ;; 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 rm -rf "$HOME/.local/go" mkdir -p "$HOME/.local" tar -C "$HOME/.local" -xzf /tmp/go.tar.gz rm /tmp/go.tar.gz export GOPATH="$HOME/.local/go-path" export PATH="$HOME/.local/go/bin:$GOPATH/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/.local/go-path}/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) ──" # Always install/upgrade via pip to get the latest version (apt repos are outdated). # Remove apt version first if present, then install via pip to /usr/local/bin. if has pip3; then # Remove outdated apt version if installed case $PM in apt) if dpkg -s yt-dlp &>/dev/null 2>&1; then echo " Removing outdated apt version..." sudo apt remove -y yt-dlp > /dev/null 2>&1 fi ;; esac echo " Installing/upgrading yt-dlp via pip..." sudo pip3 install --break-system-packages --upgrade yt-dlp 2>/dev/null if has yt-dlp; then ok "yt-dlp $(yt-dlp --version) installed"; else warn "yt-dlp pip install failed"; fi else case $PM in apt) install_pkg yt-dlp 2>/dev/null && ok "yt-dlp installed (apt — may be outdated)" || warn "yt-dlp not available" ;; 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 ~/.local/go/bin and ~/.local/go-path/bin are in your PATH for Go tools" 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 ""