split the optional host dependencies out of setup.sh

8 Rust, 9 PulseAudio, 10 cliamp, 13 yt-dlp and 17 the remote desktop move to
scripts/setup/setup-sidecars.sh, which nothing invokes — running it is a
deliberate act. They are what the optional, sidecar-backed features need on the
host, not what the app needs to serve itself.

11 Neovim, 12 the shell extras and 14 the npm globals are gone entirely. The
host provisioning already installs node, npm, pm2, Claude Code, Neovim and the
shell, and two installers racing for the same binaries is worse than one. That
makes node, npm, pm2 and the agent CLIs prerequisites of this script rather
than products of it, so the verification block still checks claude and pm2 —
section 19 warns and skips rather than failing when pm2 is absent, which would
otherwise finish "successfully" with nothing listening.

eza is the one casualty: the provisioning installs lazygit, starship, oh-my-zsh
and nvim, but not that.

Section numbers keep their gaps so the two files read against each other. One
line survives from the removed section 14 — the ~/.local/bin PATH export, which
section 19's `has pm2` and the agent's claude lookup both still depend on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 07:24:19 +02:00
co-authored by Claude Opus 5
parent cec8fbe57e
commit 4e404f17c8
2 changed files with 301 additions and 333 deletions
+258
View File
@@ -0,0 +1,258 @@
#!/bin/bash
# Officer — host dependencies for the optional, sidecar-backed features.
#
# Usage:
# bash scripts/setup/setup-sidecars.sh
#
# WHAT THIS IS
# Everything here was part of setup.sh and is not any more. setup.sh installs what the app needs to
# serve itself; this installs what a handful of *optional* features need on the host, and it is never
# invoked by setup.sh — running it is a deliberate act.
#
# The sections keep the numbering they had in setup.sh so the two files can be read against each
# other:
#
# 1 (was 8) Rust
# 2 (was 9) PulseAudio + audio dev headers
# 3 (was 10) cliamp
# 4 (was 13) yt-dlp
# 5 (was 17) Remote desktop (delegates to setup-desktop.sh)
#
# There is no `light`/`full` profile here. In setup.sh these sections were the ones `light` skipped,
# so gating them again would only mean "run this script and have it do nothing" — running it at all
# IS the opt-in.
#
# ORDER MATTERS: section 3 needs Go, which setup.sh installs. Run setup.sh first.
# Section 5 rewrites GRUB and switches the display manager — it takes effect on the next reboot.
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)"
# setup.sh installed Go and rustup into the user's home and exported them for its own run only. A
# fresh shell has neither on PATH, which would make `has go` false (silently skipping the cliamp
# build) and `has rustc` false (re-running rustup over an existing toolchain). Put them back.
export PATH="$HOME/.local/go/bin:$HOME/.cargo/bin:$PATH"
# ─── 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 — optional host dependencies ($PM)"
echo "═══════════════════════════════════════════"
# ─── 1. Rust (was setup.sh section 8) ─────────────────────────────────────────
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
# ─── 2. PulseAudio (headless audio for cliamp) (was section 9) ────────────────
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
# Vorbis/OGG/FLAC dev headers (needed by cliamp's Go dependencies)
case $PM in
apt)
for pkg in libvorbis-dev libogg-dev libflac-dev; do
if dpkg -s "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else PULSE_PKGS+=("$pkg"); fi
done
;;
pacman)
for pkg in libvorbis libogg flac; do
if pacman -Qi "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else PULSE_PKGS+=("$pkg"); fi
done
;;
brew) ;; # not needed on macOS
esac
if [ ${#PULSE_PKGS[@]} -gt 0 ]; then
install_pkg "${PULSE_PKGS[@]}"
ok "Installed: ${PULSE_PKGS[*]}"
fi
# ─── 3. cliamp (music player) (was section 10) ────────────────────────────────
echo ""
echo "── cliamp ──"
# Export GOPATH (not just PATH) so `go install` lands in GOPATH_BIN — even when Go was already present
# this run and install_go (which sets GOPATH) never ran. Otherwise go uses its default ~/go/bin and the
# check below wrongly reports a build failure.
export GOPATH="${GOPATH:-$HOME/.local/go-path}"
GOPATH_BIN="$GOPATH/bin"
export PATH="$GOPATH_BIN:$PATH"
if has cliamp; then
skip "cliamp ($(command -v cliamp))"
else
if ! has go; then
warn "Go not installed — skipping cliamp build (run setup.sh first)"
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
warn "cliamp build failed"
fi
fi
fi
# ─── 4. yt-dlp (video/audio download) (was section 13) ────────────────────────
echo ""
echo "── yt-dlp ──"
# 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
# ─── 5. remote desktop (Ubuntu Desktop + VNC) (was section 17) ────────────────
echo ""
echo "── Remote Desktop (Ubuntu Desktop + VNC) ──"
# No "already installed" guard here on purpose. This used to skip on `dpkg -s ubuntu-desktop`, which
# treats one package being present as proof the whole remote desktop is configured — and those are very
# different things. A host can have ubuntu-desktop and still be missing every part that makes the mirror
# work: GDM auto-login, the forced Xorg session, the captured EDID and its kernel command line, the
# login-time mode setter. That was not hypothetical; it was this machine on 2026-08-02, where the guard
# reported "skip" while five of setup-desktop.sh's steps had never run and /desktop could not survive a
# reboot. setup-desktop.sh is idempotent throughout — every step either no-ops or is individually
# guarded — so letting it run each time converges a partially configured host instead of trusting a
# proxy for state it never actually checked.
#
# This is by far the most expensive section — it pulls the whole ubuntu-desktop meta-package, rewrites
# /etc/default/grub and switches the display manager.
case $PM in
apt)
bash "$SCRIPT_DIR/setup-desktop.sh"
;;
*)
warn "Remote desktop setup is Ubuntu/Debian only — skipping"
;;
esac
# ─── verification ─────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
echo " Verification"
echo "═══════════════════════════════════════════"
echo ""
check() {
if has "$1"; then ok "$1"; else fail "$1 — NOT FOUND"; fi
}
echo "Rust:"
check rustc
check cargo
echo ""
echo "Audio (cliamp):"
check pulseaudio
check parec
check pactl
check cliamp
echo ""
echo "Download:"
check yt-dlp
echo ""
echo "═══════════════════════════════════════════"
echo " Done"
echo "═══════════════════════════════════════════"
echo ""
echo "Notes:"
echo " • PulseAudio null sink starts automatically with the server"
echo " • Make sure ~/.cargo/bin is in your PATH for Rust tools"
echo " • Make sure ~/.local/go-path/bin is in your PATH for Go-installed tools (cliamp)"
echo " • REBOOT to switch into the GNOME-on-Xorg session the remote desktop mirrors"
echo ""
+43 -333
View File
@@ -13,13 +13,24 @@
# Claude/opencode chat — on a Linux host. Installs only what those need: node, bun, ffmpeg,
# Postgres, pm2 and the two agent CLIs, then starts ecosystem.light.config.cjs.
#
# Skipped by `light`: archive extras, the sudoers entry and auto-suspend disabling, Go, Rust,
# PulseAudio, cliamp, Neovim, the shell extras (oh-my-zsh/eza/lazygit), yt-dlp, and
# the remote desktop. Of the Docker services only Postgres is brought up.
# Skipped by `light`: archive extras, the sudoers entry and auto-suspend disabling, and Go.
# Of the Docker services only Postgres is brought up.
#
# The app itself is identical — every API route stays mounted, so the features whose sidecars
# are not running report themselves unavailable rather than disappearing. A profile changes
# which processes start, not which code ships.
#
# NOT INSTALLED HERE — and the gaps in the section numbers are where these used to be
# Moved to scripts/setup/setup-sidecars.sh, which nothing below invokes; run it deliberately, and
# only after this script: 8 Rust, 9 PulseAudio, 10 cliamp, 13 yt-dlp, 17 remote desktop.
#
# Removed outright, because the host provisioning already installs them and two installers racing
# for the same binaries is worse than one: 11 Neovim, 12 shell extras (oh-my-zsh/eza/lazygit),
# 14 npm globals (the ~/.local npm prefix, Claude Code, pm2).
#
# That makes node, npm, pm2 and the agent CLIs PREREQUISITES of this script rather than products of
# it. Section 19 warns and skips rather than failing if pm2 is absent, so a host that never ran the
# provisioning will finish "successfully" with nothing listening — check the verification block.
set -e
@@ -138,7 +149,7 @@ if has make && has gcc; then skip "build tools (make, gcc, g++)"; else
esac
fi
# pkg-config — needed by cgo-based Go packages (e.g. ebitengine/oto for cliamp)
# pkg-config — needed by cgo-based Go packages (e.g. ebitengine/oto for cliamp, in setup-sidecars.sh)
if has pkg-config; then skip "pkg-config"; else
case $PM in
apt) CORE_PKGS+=(pkg-config) ;;
@@ -518,8 +529,9 @@ fi
# prompt to every account, so leaving starship out of light meant every member's shell fell back to the plain
# one on exactly the installs most likely to have members.
#
# One static binary and one config file. oh-my-zsh, eza and lazygit stay in section 12, where `light` skips
# them: those are host comforts, and the shell template treats each as optional.
# One static binary and one config file, which is why it survived the cull that removed the rest of the
# terminal tooling: oh-my-zsh, eza and lazygit are host comforts the provisioning installs, and the shell
# template treats each as optional. Starship it does not — the prompt would visibly degrade.
echo ""
echo "── Prompt (starship) ──"
@@ -532,9 +544,7 @@ else
fi
# Deploy starship config. Unconditionally cp'ing here overwrote a customised ~/.config/starship.toml on
# every run, silently — the nvim step below already gets this right by guarding on the config's
# existence, so this was just inconsistent. Converge when there is nothing to lose, keep what the user
# wrote when there is.
# every run, silently. Converge when there is nothing to lose, keep what the user wrote when there is.
mkdir -p "$HOME/.config"
STARSHIP_DEST="$HOME/.config/starship.toml"
if [ ! -f "$STARSHIP_DEST" ]; then
@@ -547,13 +557,11 @@ else
fi
# Sections 7-13 are one block because `light` skips all of them. Go and PulseAudio exist to build and
# feed cliamp; Rust has no consumer left in the tree; Neovim, the shell tooling and yt-dlp are host
# comforts and capability dependencies rather than anything the app needs to serve a file browser, a
# terminal and a chat.
# Go is a host comfort rather than anything the app needs to serve a file browser, a terminal and a
# chat, so `light` skips it. It is the only section left in this block — 8-13 were removed or moved.
if is_light; then
echo ""
omit "Go, Rust, PulseAudio, cliamp, Neovim, shell extras (oh-my-zsh/eza/lazygit), yt-dlp"
omit "Go"
else
# ─── 7. Go ─────────────────────────────────────────────────────────────────────
@@ -607,279 +615,18 @@ else
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 ──"
# 8 Rust, 9 PulseAudio, 10 cliamp and 13 yt-dlp are in setup-sidecars.sh.
# 11 Neovim, 12 shell extras (oh-my-zsh/eza/lazygit) and 14 npm globals are gone entirely — the host
# provisioning owns node, npm, pm2, Claude Code, Neovim and the shell, and this script duplicating
# them meant two installers racing for the same binaries.
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
fi # end of the light-profile skip, which is now section 7 alone
# ─── 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
# Vorbis/OGG/FLAC dev headers (needed by cliamp's Go dependencies)
case $PM in
apt)
for pkg in libvorbis-dev libogg-dev libflac-dev; do
if dpkg -s "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else PULSE_PKGS+=("$pkg"); fi
done
;;
pacman)
for pkg in libvorbis libogg flac; do
if pacman -Qi "$pkg" &>/dev/null 2>&1; then skip "$pkg"; else PULSE_PKGS+=("$pkg"); fi
done
;;
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 ──"
# Export GOPATH (not just PATH) so `go install` lands in GOPATH_BIN — even when Go was already present
# this run and install_go (which sets GOPATH) never ran. Otherwise go uses its default ~/go/bin and the
# check below wrongly reports a build failure.
export GOPATH="${GOPATH:-$HOME/.local/go-path}"
GOPATH_BIN="$GOPATH/bin"
export PATH="$GOPATH_BIN:$PATH"
if has cliamp; then
skip "cliamp ($(command -v 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
warn "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 (starship is section 6b, outside the light skip) ─────
echo ""
echo "── Terminal tools (oh-my-zsh, eza, lazygit) ──"
# 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' | sed 's/^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' | sed 's/^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
fi # end of the light-profile skip for sections 7-13
# ─── 14. npm global packages (user-local) ───────────────────────────────────
echo ""
echo "── npm global packages (user-local) ──"
# Ensure ~/.local/bin is in PATH for this session
# Kept from the removed section 14: nothing here installs into ~/.local/bin any more, but section 19
# still asks `has pm2` and the agent still resolves `claude` off PATH. A host that installed either
# user-locally would otherwise look like it has neither.
export PATH="$HOME/.local/bin:$PATH"
if ! has npm; then
warn "npm not found — skipping global package installs"
else
# Set npm prefix to user-local so no sudo is needed for installs/updates
echo " Configuring npm global prefix to ~/.local..."
npm config set prefix "$HOME/.local"
ok "npm prefix set to $HOME/.local"
# Claude Code (uses Anthropic's own installer for auto-update support)
if has claude; then
skip "claude (claude-code)"
else
echo " Installing claude-code via Anthropic installer..."
curl -fsSL https://claude.ai/install.sh | bash # bash, not sh: a piped script ignores its shebang and install.sh is bash
if has claude; then ok "claude-code installed"; else warn "claude-code install failed"; fi
fi
# No /usr/local/bin/claude symlink. That existed because the sidecar hardcoded that path, which in
# turn came from the bwrap-sandboxed architecture — the jail ro-bound /usr and could not see the
# installer's real target in ~/.local/bin. The sandbox is gone and claude-manager.ts now resolves the
# CLI itself: $CLAUDE_BIN, then PATH, then ~/.local/bin/claude, /usr/local/bin/claude and
# /opt/homebrew/bin/claude. The installer above puts it in ~/.local/bin, which is both on PATH and the
# first candidate, so the symlink was satisfying a requirement that no longer exists — at the cost of
# a sudo-owned link into /usr/local/bin, a directory macOS does not even ship.
# pm2 (process manager)
if has pm2; then
skip "pm2"
else
echo " Installing pm2..."
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 ──"
@@ -1001,34 +748,7 @@ ENVFILE
ok ".env written to $PROJECT_DIR/.env"
fi
# ─── 17. remote desktop (Ubuntu Desktop + VNC) ───────────────────────────────
echo ""
echo "── Remote Desktop (Ubuntu Desktop + VNC) ──"
# No "already installed" guard here on purpose. This used to skip on `dpkg -s ubuntu-desktop`, which
# treats one package being present as proof the whole remote desktop is configured — and those are very
# different things. A host can have ubuntu-desktop and still be missing every part that makes the mirror
# work: GDM auto-login, the forced Xorg session, the captured EDID and its kernel command line, the
# login-time mode setter. That was not hypothetical; it was this machine on 2026-08-02, where the guard
# reported "skip" while five of setup-desktop.sh's steps had never run and /desktop could not survive a
# reboot. setup-desktop.sh is idempotent throughout — every step either no-ops or is individually
# guarded — so letting it run each time converges a partially configured host instead of trusting a
# proxy for state it never actually checked.
# The light profile does not run officer-vnc, so there is nothing to mirror. This is the single most
# expensive section — it pulls the whole ubuntu-desktop meta-package — and the one most clearly outside
# "file browser, terminal, chat".
if is_light; then
omit "remote desktop (ubuntu-desktop, GDM, x11vnc, Brave)"
else
case $PM in
apt)
bash "$SCRIPT_DIR/setup-desktop.sh"
;;
*)
warn "Remote desktop setup is Ubuntu/Debian only — skipping"
;;
esac
fi
# 17 remote desktop is in setup-sidecars.sh.
# ─── 18. project initialization ──────────────────────────────────────────────
echo ""
@@ -1116,17 +836,13 @@ check gcc
echo ""
echo "Dev tools:"
# Only the ones the light profile actually installs are checked under it — reporting Go and cliamp as
# NOT FOUND on an install that deliberately skipped them makes a clean run look broken.
# Only what this script still installs is checked. Reporting Go as NOT FOUND on a light install that
# deliberately skipped it makes a clean run look broken; so does checking for nvim, lazygit and eza,
# which this script no longer owns at all.
if ! is_light; then
check go
check rustc
check cargo
check nvim
check starship
check lazygit
check eza
fi
check starship
check zsh
check rg
check fd
@@ -1137,21 +853,15 @@ check tree
check btop
check sqlite3
if ! is_light; then
# Neither of these is installed here any more — they come from the host provisioning. They are still
# checked because section 19 and every chat turn depend on them, and "NOT FOUND" here is the only
# warning you get before the services silently do not start.
echo ""
echo "Audio (cliamp):"
check pulseaudio
check parec
check pactl
check cliamp
fi
echo ""
echo "AI agents:"
echo "AI agents (from host provisioning):"
check claude
echo ""
echo "Process manager:"
echo "Process manager (from host provisioning):"
check pm2
echo ""
@@ -1161,7 +871,6 @@ check 7z
check unrar
check pgrep
check fuser
if ! is_light; then check yt-dlp; fi
echo ""
echo "═══════════════════════════════════════════"
@@ -1189,8 +898,9 @@ fi
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 " • node, npm, pm2 and the agent CLIs come from the host provisioning, not from here"
echo " • Rust, PulseAudio, cliamp, yt-dlp and the remote desktop are NOT installed by this script:"
echo " run 'bash scripts/setup/setup-sidecars.sh' if you want them"
echo ""