fix(pi): add snap node compatibility diagnostics and documentation

- Added detailed error logging to detect snap node compatibility issues
- When Pi process exits with code 1, log helpful diagnostic info including node path
- Add hint to check for snap node and reinstall via apt/nvm
- Create SNAP_NODE_COMPATIBILITY.md with full troubleshooting guide
- Document root cause: snap node has file descriptor incompatibility with Bun.spawn stdin pipes
- Provide clear installation instructions for NodeSource and nvm alternatives
This commit is contained in:
2026-03-04 01:45:36 +00:00
parent 72d1341cbc
commit ef13f96d36
34 changed files with 2394 additions and 1466 deletions
+181
View File
@@ -0,0 +1,181 @@
#!/bin/bash
# One-time migration: provision Linux users for existing database members.
# Reads users from PostgreSQL, skips Super Admins (they use the service account),
# and creates Linux users + seeds shell configs for everyone else.
#
# Usage: bash scripts/provision-existing-users.sh
# Requires: sudoers entry from setup.sh, POSTGRES_URL in .env
set -euo pipefail
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 (skipped)"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$PROJECT_DIR/.env"
TEMPLATE_DIR="$PROJECT_DIR/src/servers/api/terminal/templates"
# Load .env
if [ ! -f "$ENV_FILE" ]; then
fail ".env not found at $ENV_FILE"
exit 1
fi
source <(grep -E '^[A-Z_]+=.*' "$ENV_FILE" | sed 's/^/export /')
# Resolve DATA_PATH
DATA_PATH="${DATA_PATH:-$PROJECT_DIR/data}"
# Parse POSTGRES_URL for psql
if [ -z "${POSTGRES_URL:-}" ]; then
fail "POSTGRES_URL not set in .env"
exit 1
fi
# Extract components from postgresql://user:pass@host:port/dbname
PG_USER=$(echo "$POSTGRES_URL" | sed -n 's|.*://\([^:]*\):.*|\1|p')
PG_PASS=$(echo "$POSTGRES_URL" | sed -n 's|.*://[^:]*:\([^@]*\)@.*|\1|p')
PG_HOST=$(echo "$POSTGRES_URL" | sed -n 's|.*@\([^:]*\):.*|\1|p')
PG_PORT=$(echo "$POSTGRES_URL" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
PG_DB=$(echo "$POSTGRES_URL" | sed -n 's|.*/\([^?]*\).*|\1|p')
echo ""
echo "═══════════════════════════════════════════"
echo " Provision existing users"
echo "═══════════════════════════════════════════"
echo ""
echo "DATA_PATH: $DATA_PATH"
echo "Database: $PG_DB @ $PG_HOST:$PG_PORT"
echo ""
# Query non-Super Admin active users
USERS=$(PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -F '|' \
-c "SELECT email, COALESCE(username, '') FROM users WHERE role != 'Super Admin' AND status = 'Active';" 2>&1)
if [ $? -ne 0 ]; then
fail "Failed to query database: $USERS"
exit 1
fi
if [ -z "$USERS" ]; then
echo "No non-admin active users found. Nothing to do."
exit 0
fi
echo "── Found users ──"
echo "$USERS" | while IFS='|' read -r email username; do
echo " $email (username: ${username:-<from email>})"
done
echo ""
echo "── Provisioning ──"
while IFS='|' read -r email username; do
# Derive shell username (same logic as toShellUsername in data-path.ts)
if [ -n "$username" ]; then
shell_user=$(echo "$username" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32)
else
shell_user=$(echo "$email" | sed 's/@.*$//' | sed 's/[^a-zA-Z0-9._-]/_/g' | tr '[:upper:]' '[:lower:]' | cut -c1-32)
fi
USER_ROOT="$DATA_PATH/$email"
HOME_DIR="$USER_ROOT/home"
echo ""
echo " [$email$shell_user]"
# Ensure data dirs exist (sudo in case dir is owned by a previous provisioning run)
sudo mkdir -p "$USER_ROOT" "$HOME_DIR"
# Create Linux user if needed
if id "$shell_user" &>/dev/null; then
skip "Linux user $shell_user already exists"
else
if sudo useradd -d "$HOME_DIR" -s /bin/zsh -M "$shell_user"; then
ok "Created Linux user $shell_user"
else
fail "Failed to create Linux user $shell_user"
continue
fi
fi
# Seed shell configs (only if not already present)
if [ ! -f "$HOME_DIR/.zshenv" ] && [ -f "$TEMPLATE_DIR/.zshenv" ]; then
sudo cp "$TEMPLATE_DIR/.zshenv" "$HOME_DIR/.zshenv"
ok "Seeded .zshenv"
else
skip ".zshenv"
fi
if [ ! -f "$HOME_DIR/.zshrc" ] && [ -f "$TEMPLATE_DIR/.zshrc" ]; then
sudo cp "$TEMPLATE_DIR/.zshrc" "$HOME_DIR/.zshrc"
ok "Seeded .zshrc"
else
skip ".zshrc"
fi
if [ ! -f "$HOME_DIR/.tmux.conf" ] && [ -f "$TEMPLATE_DIR/.tmux.conf" ]; then
sudo cp "$TEMPLATE_DIR/.tmux.conf" "$HOME_DIR/.tmux.conf"
ok "Seeded .tmux.conf"
else
skip ".tmux.conf"
fi
sudo mkdir -p "$HOME_DIR/.config"
if [ ! -f "$HOME_DIR/.config/starship-officer.toml" ] && [ -f "$TEMPLATE_DIR/starship-officer.toml" ]; then
sudo cp "$TEMPLATE_DIR/starship-officer.toml" "$HOME_DIR/.config/starship-officer.toml"
ok "Seeded starship config"
else
skip "starship config"
fi
# Oh My Zsh
if [ ! -d "$HOME_DIR/.oh-my-zsh" ]; then
if [ -d "$HOME/.oh-my-zsh" ]; then
sudo cp -r "$HOME/.oh-my-zsh" "$HOME_DIR/.oh-my-zsh"
ok "Copied oh-my-zsh from host"
fi
else
skip "oh-my-zsh"
fi
# LazyVim
sudo mkdir -p "$HOME_DIR/.config"
if [ ! -d "$HOME_DIR/.config/nvim" ]; then
if [ -d "$HOME/.config/nvim" ]; then
sudo cp -r "$HOME/.config/nvim" "$HOME_DIR/.config/nvim"
ok "Copied nvim config from host"
fi
else
skip "nvim config"
fi
# Ensure dirs
sudo mkdir -p "$HOME_DIR/.local/bin"
sudo mkdir -p "$HOME_DIR/.pi/agent/sessions"
# Set ownership and permissions last
sudo chown -R "$shell_user:$shell_user" "$USER_ROOT"
sudo chmod 770 "$USER_ROOT"
# Add service user to this user's group so server jobs can access user data
SERVICE_USER="${SUDO_USER:-$(whoami)}"
if [ "$SERVICE_USER" != "$shell_user" ]; then
sudo usermod -aG "$shell_user" "$SERVICE_USER"
ok "Added $SERVICE_USER to group $shell_user"
fi
ok "Provisioning complete"
done <<< "$USERS"
echo ""
echo "═══════════════════════════════════════════"
echo " Done!"
echo "═══════════════════════════════════════════"
echo ""
+1 -8
View File
@@ -5,14 +5,7 @@ set -euo pipefail
# Run as the user who will own the VNC session (not root).
# Usage: ./scripts/setup-desktop.sh <vnc-password> [resolution]
if [ $# -lt 1 ]; then
echo "Usage: $0 <vnc-password> [resolution]"
echo " vnc-password: 8 characters max (VNC protocol limitation)"
echo " resolution: optional, default 1920x1080"
exit 1
fi
VNC_PASS="$1"
VNC_PASS="${1:-$(head -c 6 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 8)}"
RESOLUTION="${2:-1920x1080}"
USER_NAME="$(whoami)"
ENV_FILE="$(cd "$(dirname "$0")/.." && pwd)/.env"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Officer PTY Sidecar — systemd service setup
# Creates and enables a systemd service for the terminal PTY sidecar.
# Usage: bash scripts/setup-pty-sidecar.sh
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
warn() { echo -e " ${YELLOW}!${NC} $1"; }
skip() { echo -e " - $1 (already set up)"; }
SERVICE_NAME="officer-pty-sidecar"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
SERVICE_USER="$(whoami)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
NODE_BIN="$(which node)"
echo ""
echo "── PTY Sidecar (systemd service) ──"
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
skip "$SERVICE_NAME service already running"
echo " Use 'bun run restart:sidecar' to restart"
exit 0
fi
sudo tee "$SERVICE_FILE" > /dev/null << EOF
[Unit]
Description=Officer PTY Sidecar
After=network.target
[Service]
Type=simple
User=$SERVICE_USER
WorkingDirectory=$PROJECT_DIR
Environment=TERMINAL_PTY_PORT=5338
ExecStart=$NODE_BIN $PROJECT_DIR/src/servers/api/terminal/pty-sidecar.mjs
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now "$SERVICE_NAME"
if systemctl is-active --quiet "$SERVICE_NAME"; then
ok "$SERVICE_NAME service installed and running"
else
warn "$SERVICE_NAME service failed to start — check 'journalctl -u $SERVICE_NAME'"
fi
+391 -34
View File
@@ -55,8 +55,12 @@ if has git; then skip "git"; else CORE_PKGS+=(git); fi
if has zip; then skip "zip"; else CORE_PKGS+=(zip); fi
if has unzip; then skip "unzip"; else CORE_PKGS+=(unzip); fi
# curl / wget (usually present but just in case)
# 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
@@ -82,11 +86,116 @@ if has script; then skip "script (bsdutils)"; else
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) ──"
@@ -127,30 +236,26 @@ else
ok "ffmpeg installed"
fi
# ─── 4. docker ─────────────────────────────────────────────────────────────────
# ─── 4. sudoers for officer service user ──────────────────────────────────────
echo ""
echo "── Docker ──"
echo "── Sudoers (Linux user isolation) ──"
if has docker; then
skip "docker ($(docker --version 2>/dev/null | awk '{print $3}' | tr -d ','))"
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)
warn "Installing docker.io from apt"
sudo apt install -y docker.io
sudo usermod -aG docker "$USER"
warn "You may need to log out/in for docker group to take effect"
;;
pacman)
sudo pacman -S --noconfirm docker
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
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 "Install Docker Desktop from https://www.docker.com/products/docker-desktop"
warn "Sudoers setup is Linux-only — skipping on macOS"
;;
esac
if has docker; then ok "docker installed"; else warn "docker not found — install manually"; fi
fi
# ─── 5. Node.js 22 ────────────────────────────────────────────────────────────
@@ -172,6 +277,45 @@ else
warn " nvm install 22"
fi
# ─── npm global prefix ─────────────────────────────────────────────────────────
# Set npm global prefix to a user-writable directory so npm install -g
# never requires sudo. This also lets the server install packages at runtime.
echo ""
echo "── npm global prefix ──"
if has npm; then
NPM_PREFIX=$(npm config get prefix 2>/dev/null)
NPM_GLOBAL="$HOME/.npm-global"
if [ "$NPM_PREFIX" = "$NPM_GLOBAL" ]; then
skip "npm prefix already set to $NPM_GLOBAL"
else
mkdir -p "$NPM_GLOBAL"
npm config set prefix "$NPM_GLOBAL"
export PATH="$NPM_GLOBAL/bin:$PATH"
ok "Set npm global prefix to $NPM_GLOBAL"
# Migrate existing global packages from system prefix if any
if [ -d "$NPM_PREFIX/lib/node_modules/@mariozechner" ] || [ -d "$NPM_PREFIX/lib/node_modules/@anthropic-ai" ]; then
warn "Removing stale system-level npm packages from $NPM_PREFIX (will reinstall to $NPM_GLOBAL)"
sudo rm -rf "$NPM_PREFIX/lib/node_modules/@mariozechner" "$NPM_PREFIX/lib/node_modules/@anthropic-ai" 2>/dev/null
sudo rm -f "$NPM_PREFIX/bin/pi" "$NPM_PREFIX/bin/claude" 2>/dev/null
fi
fi
# Ensure ~/.npm-global/bin is in shell profiles
for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
if [ -f "$profile" ] && ! grep -q '.npm-global/bin' "$profile"; then
echo '' >> "$profile"
echo '# npm global packages' >> "$profile"
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$profile"
ok "Added ~/.npm-global/bin to $(basename "$profile")"
fi
done
else
warn "npm not found — skipping prefix setup"
fi
# ─── 6. Bun ───────────────────────────────────────────────────────────────────
echo ""
echo "── Bun ──"
@@ -189,14 +333,45 @@ fi
echo ""
echo "── Go ──"
GOLANG_VERSION=1.23.6
if has go; then
skip "go ($(go version 2>/dev/null | awk '{print $3}'))"
else
install_pkg golang-go 2>/dev/null || install_pkg go 2>/dev/null || install_pkg golang 2>/dev/null
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. PulseAudio (headless audio for cliamp) ────────────────────────────────
# ─── 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) ──"
@@ -235,7 +410,7 @@ if [ ${#PULSE_PKGS[@]} -gt 0 ]; then
ok "Installed: ${PULSE_PKGS[*]}"
fi
# ─── 9. cliamp (music player) ────────────────────────────────────────────────
# ─── 10. cliamp (music player) ────────────────────────────────────────────────
echo ""
echo "── cliamp ──"
@@ -261,7 +436,114 @@ else
fi
fi
# ─── 10. yt-dlp (optional — video/audio download) ─────────────────────────────
# ─── 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) ──"
@@ -275,22 +557,58 @@ else
esac
fi
# ─── 11. npm dependencies ─────────────────────────────────────────────────────
# ─── 14. npm global packages ─────────────────────────────────────────────────
echo ""
echo "── npm global packages ──"
if has npm; then
# Pi (coding agent)
if has pi; then
skip "pi (@mariozechner/pi-coding-agent)"
else
npm install -g @mariozechner/pi-coding-agent
if has pi; then ok "pi installed"; else warn "pi install failed"; fi
fi
# Fix ~/.pi ownership (previous installs via sudo may have created root-owned dirs)
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"
fi
fi
# Patch Pi compaction bug: calculateContextTokens crashes when usage is undefined
PI_COMPACTION="$(npm root -g 2>/dev/null)/@mariozechner/pi-coding-agent/dist/core/compaction/compaction.js"
if [ -f "$PI_COMPACTION" ]; then
if grep -q "if (!usage) return 0;" "$PI_COMPACTION" 2>/dev/null; then
skip "pi compaction patch (already applied)"
else
sed -i '/^export function calculateContextTokens(usage) {$/a\ if (!usage) return 0;' "$PI_COMPACTION"
ok "Applied pi compaction bug patch"
fi
fi
# Claude Code
if has claude; then
skip "claude (@anthropic-ai/claude-code)"
else
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
npm install -g pm2
if has pm2; then ok "pm2 installed"; else warn "pm2 install failed"; fi
fi
else
warn "npm not found — skipping global package installs"
fi
# ─── 12. bun install (project dependencies) ──────────────────────────────────
# ─── 15. bun install (project dependencies) ──────────────────────────────────
echo ""
echo "── Project dependencies ──"
@@ -305,6 +623,26 @@ 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 "═══════════════════════════════════════════"
@@ -321,19 +659,45 @@ check git
check node
check bun
check npm
check docker
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 go
check pulseaudio
check parec
check pactl
check cliamp
echo ""
echo "AI agents:"
check pi
check claude
check pm2
echo ""
echo "Optional:"
check unzip
@@ -342,13 +706,6 @@ check unrar
check pgrep
check fuser
check yt-dlp
check pi
echo ""
echo "Installable from Settings UI:"
for tool in claude opencode; do
if has "$tool"; then ok "$tool"; else echo " - $tool (install from Settings > Applications)"; fi
done
echo ""
echo "═══════════════════════════════════════════"
@@ -358,6 +715,6 @@ echo ""
echo "Notes:"
echo " • PulseAudio null sink starts automatically with the server"
echo " • Make sure ~/go/bin is in your PATH for cliamp"
echo " • Claude, opencode, sharp, whisper-cpp, mlx-audio"
echo " can be installed from the Settings > Applications page"
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 ""