Replace stale bin/install.sh with a wrapper over sync.py restore

bin/install.sh still rsynced --delete from a backup path retired in July, so
the documented Linux install both failed and could erase a working tree. Every
step it claimed to do already lives in bin/sync.py, shared with Windows.

Root ./install.sh is now a thin wrapper over `sync.py restore`, so deployment
stays one bash command. It also registers ncp/promethean in ~/.bashrc, which
was the only thing the old script uniquely did.

Split restore-linux.sh into runtime (Ollama binary, shell aliases) and desktop
(XFCE panel, theme, os-release). Only desktop writes outside the repo, it now
auto-skips off XFCE, and `--no-desktop` skips it explicitly. Two tests keep the
stage names in sync and the $HOME writes confined to the desktop stage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-22 08:46:46 -05:00
co-authored by Claude Opus 4.8
parent c75f17de6e
commit ee6e5bead7
7 changed files with 133 additions and 193 deletions
+9 -3
View File
@@ -72,8 +72,9 @@ does all inference over HTTP).
### Linux
```bash
# 1. Install deps into the Promethean venv (auto-selects AMD/NVIDIA/CPU)
./bin/install.sh
# 1. Build everything: venv (auto-selects AMD/NVIDIA/CPU), web UI, memory DB,
# system packages and the Ollama binary.
./install.sh
# 2. Launch (memory :8001, backend :8000 — backend also serves the built UI)
./launch_nexus.sh
@@ -82,7 +83,12 @@ does all inference over HTTP).
Python deps are layered: `requirements-base.txt` (GPU-agnostic core) plus one
GPU overlay — `requirements-amd.txt` (ROCm) or `requirements-nvidia.txt` (CUDA).
`requirements-wsl.txt` is the standalone CPU-only runtime (no base overlay).
`bin/install.sh` picks the right one for the host.
`bin/sync.py` picks the right one for the host.
`./install.sh` is also the update path — re-run it any time to pull the latest
and rebuild. `--check` dry-runs it; `--no-desktop` skips the XFCE panel/theme
wiring (that stage is auto-skipped off XFCE anyway). It's a thin wrapper over
`bin/sync.py restore`, the same code the Windows box runs.
### Individual services
+1 -1
View File
@@ -10,7 +10,7 @@ cd "$(dirname "$0")/.."
fail=0
if [ ! -x Promethean/bin/python ]; then
echo "!! no Promethean venv - run bin/install.sh first" >&2
echo "!! no Promethean venv - run ./install.sh first" >&2
exit 1
fi
-168
View File
@@ -1,168 +0,0 @@
#!/bin/bash
# NexusOS installer — Linux side.
# Syncs the repo, builds the Python venv, installs frontend deps, registers ncp,
# and installs the Promethean Terminal + panel.
#
# ./install.sh Linux install (default)
# ./install.sh -w | --windows Hand off to the Windows installer (install-windows.ps1)
NEXUS_ROOT="$HOME/nexus-core"
ROUTER_BACKUP="router:/tmp/mnt/Wingdrive2/nexus-core/"
# ─── Helpers ──────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: install.sh [-w|--windows] [-h|--help]
(no flags) Run the Linux install.
-w, --windows Print how to run the native-Windows installer
(install-windows.ps1 — winget-based, no WSL).
-h, --help Show this help.
EOF
}
detect_requirements() {
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null 2>&1; then
echo "requirements-nvidia.txt"
elif lspci 2>/dev/null | grep -qi nvidia; then
echo "requirements-nvidia.txt"
elif grep -qi microsoft /proc/version 2>/dev/null; then
echo "requirements-wsl.txt"
elif lspci 2>/dev/null | grep -qi amd; then
echo "requirements-amd.txt"
else
echo "requirements-wsl.txt"
fi
}
run_windows() {
local ps1="$NEXUS_ROOT/install-windows.ps1"
if [ ! -f "$ps1" ]; then
echo "Error: $ps1 not found." >&2
exit 1
fi
# install-windows.ps1 is a native-Windows installer: it self-elevates to
# Administrator and uses winget (Python/Node/Ollama) directly. No WSL.
# It must run from Windows PowerShell, so just point the way.
cat <<EOF
The Windows installer must be run from Windows, not from this shell.
1. Open a PowerShell window in the nexus-core folder.
2. Run: powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
It self-elevates, installs Python/Node/Ollama via winget, builds the venv and
web UI, and drops a desktop shortcut. NexusOS then runs single-process on :8000.
EOF
}
# ─── Arg parsing ──────────────────────────────────────────────────────────────
case "${1:-}" in
-w|--windows)
run_windows
exit $?
;;
-h|--help)
usage
exit 0
;;
"")
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
# ─── Step 1: Sync from router ─────────────────────────────────────────────────
echo "Pulling Nexus from router..."
mkdir -p "$NEXUS_ROOT"
rsync -avz --delete \
--exclude='.git/' \
--exclude='Promethean/' \
--exclude='models/blobs/' \
--exclude='ollama/' \
--exclude='interface/web/node_modules/' \
--exclude='interface/web/dist/' \
--exclude='runtime/' \
--exclude='__pycache__/' \
--exclude='*.pyc' \
-e ssh \
"$ROUTER_BACKUP" "$NEXUS_ROOT/"
# ─── Step 2: Python venv ──────────────────────────────────────────────────────
echo ""
echo "Creating Python environment..."
python3 -m venv "$NEXUS_ROOT/Promethean"
# ─── Step 3: pip install ──────────────────────────────────────────────────────
echo ""
echo "Installing Python dependencies..."
req=$(detect_requirements)
echo "Detected: $req"
if [ -f "$NEXUS_ROOT/$req" ]; then
"$NEXUS_ROOT/Promethean/bin/pip" install --upgrade pip -q
"$NEXUS_ROOT/Promethean/bin/pip" install -r "$NEXUS_ROOT/$req"
else
echo "Warning: $req not found — skipping pip install."
fi
# ─── Step 4: npm install ──────────────────────────────────────────────────────
echo ""
echo "Installing frontend dependencies..."
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
if command -v npm &>/dev/null; then
cd "$NEXUS_ROOT/interface/web" && npm install
else
echo "npm not found — install nvm/node then run 'cd $NEXUS_ROOT/interface/web && npm install'."
fi
# ─── Step 5: Register ncp in ~/.bashrc ───────────────────────────────────────
echo ""
echo "Registering ncp..."
if ! grep -q "nexus-core/management/nexus-cli.sh" "$HOME/.bashrc"; then
cat >> "$HOME/.bashrc" << 'EOF'
# Nexus
ncp() {
~/nexus-core/management/nexus-cli.sh "$@"
}
EOF
echo "ncp registered in ~/.bashrc."
else
echo "ncp already in ~/.bashrc — skipping."
fi
if ! grep -qF "alias promethean='source ~/nexus-core/.promethean_bashrc'" "$HOME/.bashrc"; then
printf '\n# Promethean\nalias promethean='"'"'source ~/nexus-core/.promethean_bashrc'"'"'\n' >> "$HOME/.bashrc"
echo "promethean registered in ~/.bashrc."
else
echo "promethean already in ~/.bashrc — skipping."
fi
# ─── Step 6: Promethean Terminal + panel ─────────────────────────────────────
echo ""
echo "Installing Promethean Terminal..."
bash "$NEXUS_ROOT/bin/promethean/install.sh" || \
echo "Warning: Promethean Terminal install failed — run bin/promethean/install.sh manually."
echo ""
echo "Installing NexusOS panel applet..."
bash "$NEXUS_ROOT/bin/panel/install.sh" || \
echo "Warning: panel install failed — run bin/panel/install.sh manually."
# ─── Done ─────────────────────────────────────────────────────────────────────
echo ""
echo "Installation complete. Nexus is ready."
echo "Run 'ncp start' to launch Nexus."
+63 -18
View File
@@ -3,17 +3,29 @@
# The Linux-only half of a restore: system packages, the bundled Ollama binary,
# and the XFCE desktop wiring (panel, wallpaper, theme, terminal, branding).
# 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 two stages:
# 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 desktop After the rebuild: Ollama binary + desktop wiring.
# restore-linux.sh runtime After: Ollama binary + shell wiring. Any Linux box
# needs these to actually run NexusOS.
# restore-linux.sh desktop After: the XFCE look (panel, wallpaper, theme,
# branding). Cosmetic, this box's setup, and the only
# stage that rewrites files outside the repo and $HOME.
# Skippable with `--no-desktop`; auto-skipped off XFCE.
#
# 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|desktop}"
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
@@ -29,14 +41,55 @@ if [ "$stage" = "prep" ]; then
exit 0
fi
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
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
else
echo "Warning: bin/fetch-ollama.sh not found — skipping Ollama fetch."
fi
# Shell wiring: the `ncp` function and the Promethean venv alias. This is the
# only place they get registered now that bin/install.sh is gone. 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.
if ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
cat >> "$HOME/.bashrc" <<EOF
# Nexus
ncp() {
$NEXUS_ROOT/management/nexus-cli.sh "\$@"
}
EOF
echo "Registered ncp in ~/.bashrc."
fi
if ! grep -qF "alias promethean=" "$HOME/.bashrc" 2>/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: XFCE only, and it writes outside the repo -----------------
# Everything below reconfigures the logged-in user's desktop. On any other DE it
# would be actively wrong (copying XFCE channel XMLs onto a GNOME box does
# nothing good), so bail rather than make a mess of someone else's machine.
if ! command -v xfconf-query >/dev/null; then
echo "Not an XFCE session (no xfconf-query) — skipping desktop wiring."
exit 0
fi
# Panel layout, wallpaper, compositing, keybindings and terminal profile all live
@@ -45,7 +98,7 @@ fi
# 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" ] && command -v xfconf-query >/dev/null; then
if [ -d "$xml_snap" ]; then
echo ""
echo "Restoring XFCE desktop settings (panel, wallpaper, effects)..."
mkdir -p "$xml_dst"
@@ -110,11 +163,3 @@ if grep -q '^ID=linuxmint' /etc/os-release 2>/dev/null && ! grep -q 'NexusOS' /e
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
# The AMD box runs CPU-only (Vega 20, 4GB VRAM thrashes). An NVIDIA box should not.
if command -v nvidia-smi >/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
+5
View File
@@ -237,6 +237,8 @@ def cmd_restore(args) -> int:
return 1
restore_db()
rebuild_env()
linux_stage("restore-linux.sh", "runtime")
if not args.no_desktop:
linux_stage("restore-linux.sh", "desktop")
print("\nRestore complete. Nexus is ready to start.")
print("Note: Ollama models are not in the backup - pull them with `ollama pull <model>`.")
@@ -323,6 +325,9 @@ def main() -> int:
restore = sub.add_parser("restore", help="pull from Gitea, rebuild DB + venv + web UI")
restore.add_argument("-c", "--check", action="store_true", help="dry run, change nothing")
restore.add_argument("--no-desktop", action="store_true",
help="skip the XFCE desktop wiring (panel, theme, os-release). "
"Use for a test clone - that stage writes to $HOME, not the repo.")
restore.set_defaults(func=cmd_restore)
backup = sub.add_parser("backup", help="dump DB, commit and push to Gitea")
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# NexusOS installer, Linux. One painless command:
#
# git clone <repo> nexus-core && cd nexus-core && ./install.sh
#
# ponytail: a wrapper, not an installer. Every step - git pull, venv, pip with
# the right GPU overlay, npm build, apt packages, Ollama binary, desktop wiring
# - lives in bin/sync.py, shared with native Windows. Duplicating any of it here
# means two installers drifting apart, which is exactly how the old
# bin/install.sh ended up rsyncing from a backup path retired months earlier.
# Re-run it any time to update; --check dry-runs it. Windows: install-windows.ps1.
set -euo pipefail
cd "$(dirname "$0")"
if ! command -v python3 >/dev/null; then
echo "python3 is required (it runs the installer). Install it, then re-run:" >&2
echo " sudo apt-get install -y python3 python3-venv" >&2
exit 1
fi
# Prefer the venv interpreter once it exists, same as ncp does; python3 is the
# bootstrap case on a fresh clone. sync.py is stdlib-only either way.
py=Promethean/bin/python
[ -x "$py" ] || py=python3
exec "$py" bin/sync.py restore "$@"
+26
View File
@@ -209,3 +209,29 @@ def test_sync_compare_detects_direction(tmp_path):
db.unlink()
assert sync.compare(db, dump) == "no-live"
def test_restore_stages_match_the_script():
"""sync.py invokes restore-linux.sh stages by name with check=False, so a
rename on one side alone fails silently - and the runtime stage is what
installs Ollama and the ncp alias. Keep the two in agreement."""
import re
called = set(re.findall(
r'linux_stage\(\s*"restore-linux\.sh"\s*,\s*"(\w+)"',
(REPO_ROOT / "bin" / "sync.py").read_text()))
script = (REPO_ROOT / "bin" / "restore-linux.sh").read_text()
handled = set(re.search(r"case \"\$stage\" in\s*\n\s*([\w|]+)\)", script).group(1).split("|"))
assert called, "no restore-linux.sh stages found in sync.py"
assert called <= handled, f"sync.py calls unhandled stage(s): {called - handled}"
def test_desktop_stage_is_the_only_one_touching_home():
"""--no-desktop is only a real safety valve if the $HOME writes all live in
the desktop stage. A test clone runs prep + runtime unconditionally."""
script = (REPO_ROOT / "bin" / "restore-linux.sh").read_text()
runtime = script.split('if [ "$stage" = "runtime" ]; then')[1].split("\n exit 0\nfi")[0]
# The ncp/promethean shell aliases are the one deliberate exception - they're
# how you launch NexusOS at all, and both are grep-guarded no-ops on re-run.
home_writes = [ln for ln in runtime.splitlines()
if "$HOME" in ln and ".bashrc" not in ln]
assert not home_writes, f"runtime stage writes to $HOME: {home_writes}"