diff --git a/scripts/setup/setup-sidecars.sh b/scripts/setup/setup-sidecars.sh new file mode 100755 index 00000000..f1301128 --- /dev/null +++ b/scripts/setup/setup-sidecars.sh @@ -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 "" diff --git a/scripts/setup/setup.sh b/scripts/setup/setup.sh index a8e27af9..7e769b4a 100755 --- a/scripts/setup/setup.sh +++ b/scripts/setup/setup.sh @@ -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 - echo "" - echo "Audio (cliamp):" - check pulseaudio - check parec - check pactl - check cliamp -fi - +# 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 "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 ""