#!/bin/bash # The Linux-only half of a restore: system packages, the bundled Ollama binary, # and the desktop wiring (panel, wallpaper, theme, terminal, branding) for # whichever of XFCE / Plasma is installed. # None of it means anything on Windows, which is why it lives here instead of in # bin/sync.py — sync.py owns the portable half and calls this in three stages: # # restore-linux.sh prep Before the rebuild: system packages the build needs. # restore-linux.sh runtime After: Ollama binary + shell wiring. Any Linux box # needs these to actually run NexusOS. # restore-linux.sh desktop After: the desktop look (Plasma global theme, # XFCE panel/wallpaper/theme, boot + distro # branding). Cosmetic, this box's setup, and the # only stage that rewrites files outside the repo # and $HOME. Skippable with `--no-desktop`; each # desktop's branch auto-skips when absent. # # Not meant to be run by hand — use `ncp restore` (python bin/sync.py restore). # Set by bin/sync.py to the repo it was invoked from, so a clone in a scratch dir # operates on itself instead of reaching into the real ~/nexus-core. NEXUS_ROOT="${NEXUS_ROOT:-$HOME/nexus-core}" stage="${1:?usage: restore-linux.sh prep|runtime|desktop}" # sync.py calls these by name with check=False, so a stage renamed on one side # and not the other would silently do nothing. Fail loudly instead; a test keeps # this list and sync.py's call sites in agreement. case "$stage" in prep|runtime|desktop) ;; *) echo "unknown stage: $stage (expected prep|runtime|desktop)" >&2; exit 2 ;; esac cd "$NEXUS_ROOT" || { echo "No $NEXUS_ROOT"; exit 1; } if [ "$stage" = "prep" ]; then # Fresh machine: the system packages the desktop + build steps need. No-op once # installed, so it's cheap on an in-place restore. Skipped without apt (non-Debian). if command -v apt-get >/dev/null; then echo "Installing system packages..." # python3-venv is a transitional package pointing at the real # pythonX.Y-venv for whatever Python ships by default; on a distro ahead # of that transition it resolves to nothing ("has no installation # candidate") even though a real, version-suffixed package exists. # apt-cache show only succeeds for a package with an actual archive # entry, so it tells the two cases apart without guessing. venv_pkg="python3-venv" if command -v python3 >/dev/null && ! apt-cache show python3-venv >/dev/null 2>&1; then venv_pkg="python3-$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')-venv" fi pkgs="git sqlite3 curl $venv_pkg python3-gi gir1.2-gtk-3.0" # XFCE-only cosmetics: only worth attempting where there's an XFCE # session to plug into. Same signal the desktop stage below already # gates on, so a headless/WSL box (no xfconf-query) isn't left with # noisy "unable to locate package" failures for a panel it'll never run. if command -v xfconf-query >/dev/null; then pkgs="$pkgs xfce4-genmon-plugin plank blueman" fi # shellcheck disable=SC2086 # $pkgs is a deliberate word-split list sudo apt-get install -y --no-install-recommends $pkgs \ || echo "Warning: some system packages failed — install them manually." # Node.js 20+: Vite needs 20.19+, but Debian/Ubuntu apt ship an EOL Node 18 # (its 'nodejs' package) which crashes Vite with 'CustomEvent is not defined'. # Install from NodeSource only when the current node is missing or too old. node_major=$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/') if [ -z "$node_major" ] || [ "$node_major" -lt 20 ]; then echo "Installing Node.js 20 (found: ${node_major:-none})..." curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - \ && sudo apt-get install -y nodejs \ || echo "Warning: Node 20 install failed — install Node 20+ manually (Vite needs it)." fi fi exit 0 fi if [ "$stage" = "runtime" ]; then echo "" echo "Ensuring Ollama binary..." # ollama/ is gitignored, so a fresh clone has no binary — fetch it. No-op if the # machine already has one (e.g. an in-place restore). if [ -x "$NEXUS_ROOT/bin/fetch-ollama.sh" ]; then "$NEXUS_ROOT/bin/fetch-ollama.sh" "$NEXUS_ROOT" || echo "Warning: Ollama fetch failed — install it manually." else echo "Warning: bin/fetch-ollama.sh not found — skipping Ollama fetch." fi # `ncp` on PATH, not a shell function: /usr/local/bin is visible to sh, cron, # systemd units and .desktop Exec lines, none of which read ~/.bashrc. The # Windows installer does the matching thing with management\ncp.cmd on PATH. # Falls back to the old ~/.bashrc function when there's no sudo (a shell # function shadows the symlink anyway, so having both is harmless). ncp_link="/usr/local/bin/ncp" ncp_target="$NEXUS_ROOT/management/nexus-cli.sh" if [ "$(readlink -f "$ncp_link" 2>/dev/null)" = "$ncp_target" ]; then echo "ncp already on PATH at $ncp_link." elif sudo ln -sfn "$ncp_target" "$ncp_link"; then echo "Registered ncp at $ncp_link." elif ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then # Guarded by a grep so an in-place restore is a no-op and a scratch clone # can't re-point an already-wired ~/.bashrc at itself. cat >> "$HOME/.bashrc" </dev/null; then printf "\n# Promethean\nalias promethean='source %s/.promethean_bashrc'\n" \ "$NEXUS_ROOT" >> "$HOME/.bashrc" echo "Registered promethean in ~/.bashrc." fi # The AMD box runs CPU-only (Vega 20, 4GB VRAM thrashes). An NVIDIA box should not. if command -v nvidia-smi >/dev/null && command -v sqlite3 >/dev/null && \ [ "$(sqlite3 "$NEXUS_ROOT/synapse/memory/memory.db" \ "select value from settings where key='memory_gpu_offload'" 2>/dev/null)" = "0" ]; then echo "Note: memory_gpu_offload=0 came from the AMD box (4GB VRAM). This machine has an" echo " NVIDIA GPU — raise it in Settings to actually use the card." fi exit 0 fi # --- desktop stage: reconfigures the logged-in user's desktop ------------------ # Keyed on what is installed, not on one either/or: XFCE and Plasma can both be # present (this box runs both and picks at the SDDM greeter), and each branch # writes only to its own config. Anything neither branch matches falls through # to the boot/distro branding at the bottom, which is DE-independent. # Plasma. install-plasma.sh is idempotent and applies live when a session is # running; --no-sddm because bin/boot-branding.sh owns the SDDM theme, and # letting both write it was how the setting ended up in two files. if command -v lookandfeeltool >/dev/null && [ -x "$NEXUS_ROOT/assets/themes/KDE/install-plasma.sh" ]; then echo "" echo "Restoring NexusOS Plasma theme..." "$NEXUS_ROOT/assets/themes/KDE/install-plasma.sh" --no-sddm \ || echo "Warning: Plasma theme restore failed." fi # XFCE. Copying XFCE channel XMLs onto a box with no XFCE does nothing good, so # this whole branch is skipped rather than half-applied. if ! command -v xfconf-query >/dev/null; then echo "No XFCE (no xfconf-query) — skipping the XFCE desktop wiring." else # Panel layout, wallpaper, compositing, keybindings and terminal profile all live # in the xfconf channel XMLs — restore them by copying the files back. xfconfd # caches channels in memory and rewrites them on exit, so it has to die first or # it clobbers what we just copied; the next xfconf-query respawns it. xml_snap="$NEXUS_ROOT/assets/themes/restore-snapshot/xfconf-xml" xml_dst="$HOME/.config/xfce4/xfconf/xfce-perchannel-xml" if [ -d "$xml_snap" ]; then echo "" echo "Restoring XFCE desktop settings (panel, wallpaper, effects)..." mkdir -p "$xml_dst" pkill -x xfconfd 2>/dev/null && sleep 1 cp -f "$xml_snap"/*.xml "$xml_dst/" 2>/dev/null # Wallpaper props are keyed by monitor name, which differs per machine — point # every backdrop this box actually has at the Nexus background. bg="$NEXUS_ROOT/assets/background.png" if [ -f "$bg" ] && [ -n "${DISPLAY:-}" ]; then xfconf-query -c xfce4-desktop -l 2>/dev/null | grep 'last-image$' | while read -r p; do xfconf-query -c xfce4-desktop -p "$p" -s "$bg" 2>/dev/null || true done fi fi # Multi-monitor primary-follow watcher (panel + Plank track the xrandr primary). if [ -f "$NEXUS_ROOT/bin/panel/plank-primary-watch.sh" ]; then mkdir -p "$HOME/.local/bin" chmod +x "$NEXUS_ROOT/bin/panel/plank-primary-watch.sh" ln -sf "$NEXUS_ROOT/bin/panel/plank-primary-watch.sh" "$HOME/.local/bin/plank-primary-watch.sh" fi plank_snap="$NEXUS_ROOT/assets/themes/restore-snapshot/plank" if [ -d "$plank_snap" ]; then # Plank owns its item list in dconf and deletes any .dockitem missing from it # on startup, so copying files alone silently restores nothing. Order here: # kill Plank (a running one prunes what we copy), put the launcher .desktop # targets back, copy the items, set the list, restart. pkill -x plank 2>/dev/null && sleep 1 mkdir -p "$HOME/.local/share/applications" cp -f "$NEXUS_ROOT/assets/themes/restore-snapshot/applications"/*.desktop \ "$HOME/.local/share/applications/" 2>/dev/null || true mkdir -p "$HOME/.config/plank" cp -rf "$plank_snap/." "$HOME/.config/plank/" # /. = contents, else it nests plank_cfg="$NEXUS_ROOT/assets/themes/restore-snapshot/plank-dconf.ini" if [ -s "$plank_cfg" ]; then dconf load /net/launchpad/plank/ < "$plank_cfg" 2>/dev/null || true fi theme_snap="$NEXUS_ROOT/assets/themes/plank" if [ -d "$theme_snap" ]; then mkdir -p "$HOME/.local/share/plank/themes" cp -rf "$theme_snap/." "$HOME/.local/share/plank/themes/" fi command -v plank >/dev/null && setsid plank >/dev/null 2>&1 /dev/null && ! grep -q 'NexusOS' /etc/os-release; then echo "" echo "Branding /etc/os-release as NexusOS..." sudo sed -i -e 's/^NAME=.*/NAME="NexusOS"/' -e 's/^PRETTY_NAME=.*/PRETTY_NAME="NexusOS 1.0"/' \ /etc/os-release || echo "Warning: os-release branding skipped." fi