#!/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; } # ─── 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 # 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 # ─── 5. Node.js 22 ──────────────────────────────────────────────────────────── echo "" echo "── Node.js 22 (system-wide) ──" if has node; then NODE_VER=$(node -v 2>/dev/null | tr -d 'v') NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) if [ "$NODE_MAJOR" = "22" ]; then skip "node v$NODE_VER (system-wide)" else warn "Node $NODE_VER found but v22 is required — upgrading..." case $PM in apt) echo " Setting up NodeSource repository..." curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs if has node && [ "$(node -v | cut -d. -f1 | tr -d 'v')" = "22" ]; then ok "Upgraded to node v$(node -v)" else fail "Node.js upgrade failed" fi ;; pacman) install_pkg nodejs npm if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; fi ;; brew) install_pkg node if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; fi ;; esac fi else echo " Node.js not found — installing v22..." case $PM in apt) echo " Setting up NodeSource repository (this may take a moment)..." curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - echo " Installing nodejs from apt..." sudo apt-get install -y nodejs if has node; then ok "node v$(node -v) installed from NodeSource" else fail "Node.js install failed" exit 1 fi ;; pacman) echo " Installing nodejs from pacman..." install_pkg nodejs npm if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi ;; brew) echo " Installing node from brew..." install_pkg node if has node; then ok "node v$(node -v) installed"; else fail "node install failed"; exit 1; fi ;; esac fi # ─── npm global packages (system-wide) ───────────────────────────────────────── # Install npm global packages to system /usr/local so all users can access them. # This is safer for multi-user production setups. echo "" echo "── npm global packages (system-wide) ──" if ! has npm; then warn "npm not found — skipping global package installs" exit 1 fi # Verify npm is pointing to system node (not user-local nvm) NPM_BIN=$(which npm) if echo "$NPM_BIN" | grep -q '.nvm'; then warn "npm is from nvm ($NPM_BIN), but setup.sh expects system npm" warn "This is fine — packages will be installed to your nvm, but won't be available to other users" warn "For production: all users should use the same system Node.js (no nvm)" fi # ─── 6. Bun ─────────────────────────────────────────────────────────────────── echo "" echo "── Bun ──" if has bun; then skip "bun ($(bun --version 2>/dev/null))" else curl -fsSL https://bun.sh/install | bash export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" if has bun; then ok "bun installed"; else fail "bun install failed"; fi fi # ─── 7. Go ───────────────────────────────────────────────────────────────────── echo "" echo "── Go ──" GOLANG_VERSION=1.23.6 if has go; then skip "go ($(go version 2>/dev/null | awk '{print $3}'))" else case $PM in apt|pacman) echo " Installing Go ${GOLANG_VERSION} from official tarball..." ARCH=$(uname -m) case $ARCH in x86_64) GO_ARCH=amd64 ;; aarch64) GO_ARCH=arm64 ;; *) GO_ARCH=amd64 ;; esac curl -fsSL "https://go.dev/dl/go${GOLANG_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz 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 ok "go 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 # 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 EZA_VERSION=0.18.15 if has eza; then skip "eza" else case $PM in apt) 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 -rf /tmp/eza.tar.gz ;; pacman) install_pkg eza ;; brew) install_pkg eza ;; esac if has eza; then ok "eza installed"; else warn "eza install failed"; fi fi # lazygit LAZYGIT_VERSION=0.44.1 if has lazygit; then skip "lazygit" else case $PM in apt) 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 -rf /tmp/lazygit.tar.gz /tmp/LICENSE /tmp/README.md ;; 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 # 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) — now optional since we use systemd services if has pm2; then skip "pm2 (optional, using systemd services instead)" else warn "pm2 optional (Officer uses systemd services for main app and sidecar)" fi fi # ─── 15. bun install (project dependencies) ────────────────────────────────── echo "" echo "── Project dependencies ──" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" 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 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 ""