refactor(management): replace curses TUIs with API-backed ncp subcommands; panel/autostart/install integration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-11 07:25:08 -05:00
parent f4f77c5196
commit 53eaed4c7f
792 changed files with 2782 additions and 2240 deletions
+217 -77
View File
@@ -31,7 +31,6 @@ _launch() {
shift 4
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
echo " $name is already running (PID $(cat "$pid_file"))"
return 0
fi
@@ -57,29 +56,106 @@ _check() {
fi
}
_stop() {
# _stop <label> <pid_file>
local label="$1" pid_file="$2"
_wait_for_port() {
# _wait_for_port <name> <port> <pid_file> <log_file>
# Polls until the service responds on the given port, then prints ready.
# On timeout, falls back to _check for the error log.
local name="$1" port="$2" pid_file="$3" log_file="$4"
local timeout=30 i=0
if curl -s --max-time 1 "http://localhost:$port/" > /dev/null 2>&1; then
echo " $name already running (:$port)"
return 0
fi
while ! curl -s --max-time 1 "http://localhost:$port/" > /dev/null 2>&1; do
i=$((i + 1))
if [ "$i" -ge "$timeout" ]; then
echo " $name timed out after ${timeout}s"
_check "$name" "$pid_file" "$log_file"
return 1
fi
sleep 1
done
echo "$name STARTED"
}
_pid_is_ours() {
# _pid_is_ours <pid> <pattern> [pattern2]
# True only if the live PID's command line matches one of the service
# patterns. PID files outlive reboots; the OS recycles the number onto an
# unrelated process (often a desktop-session process), so a bare `kill -0`
# liveness check is not enough — TERMing a recycled PID can log the user out.
local pid="$1" p1="$2" p2="$3" cmd
[ -r "/proc/$pid/cmdline" ] || return 1
cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null) || return 1
[ -n "$p1" ] && [[ "$cmd" == *"$p1"* ]] && return 0
[ -n "$p2" ] && [[ "$cmd" == *"$p2"* ]] && return 0
return 1
}
_stop_service() {
# _stop_service <label> <pid_file> <port> <pattern> [extra_pattern]
#
# PID-based stopping is unreliable here: the frontend PID file tracks npm,
# but the server is a `node vite` GRANDCHILD (npm -> sh -c vite -> node)
# that reparents and survives a parent kill; uvicorn --reload leaves a
# worker child holding the port. So we stop in three escalating passes and
# treat the PORT as the source of truth for whether the service is down.
local label="$1" pid_file="$2" port="$3" pat="$4" pat2="$5"
# 1. Graceful: SIGTERM the tracked PID and its direct children.
if [ -f "$pid_file" ]; then
local pid
pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null
echo "$label STOPPED"
else
echo "$label NOT RUNNING"
local pid; pid=$(cat "$pid_file" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null \
&& _pid_is_ours "$pid" "$pat" "$pat2"; then
pkill -TERM -P "$pid" 2>/dev/null || true
kill -TERM "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
else
echo "$label NOT RUNNING"
fi
# 2. Sweep stragglers by command line — catches reparented grandchildren,
# --reload workers, and instances started outside this script.
[ -n "$pat" ] && pkill -TERM -f "$pat" 2>/dev/null
[ -n "$pat2" ] && pkill -TERM -f "$pat2" 2>/dev/null
# 3. Backstop: whatever still holds the port IS the service — kill it by
# port. This is what makes stop reliable no matter how the process tree
# was shaped or whether the PID file was accurate.
if [ -n "$port" ]; then
if ! _wait_for_port_close "$port"; then
fuser -k "${port}/tcp" 2>/dev/null
[ -n "$pat" ] && pkill -KILL -f "$pat" 2>/dev/null
[ -n "$pat2" ] && pkill -KILL -f "$pat2" 2>/dev/null
_wait_for_port_close "$port" || true
fi
if curl -s --max-time 1 "http://localhost:$port/" >/dev/null 2>&1; then
echo "$label STILL RUNNING (:$port) — try 'ncp kill'"
return 1
fi
fi
echo "$label STOPPED"
return 0
}
_wait_for_port_close() {
# _wait_for_port_close <port>
# Waits until the port stops responding. Silent — caller prints the result.
local port="$1" timeout=15 i=0
while curl -s --max-time 1 "http://localhost:$port/" > /dev/null 2>&1; do
i=$((i + 1))
[ "$i" -ge "$timeout" ] && return 1
sleep 1
done
return 0
}
_status() {
# _status <name> <pid_file>
local name="$1" pid_file="$2"
# _status <name> <pid_file> [port]
local name="$1" pid_file="$2" port="${3:-}"
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
echo " $name: RUNNING (PID $(cat "$pid_file"))"
elif [ -n "$port" ] && curl -s --max-time 1 "http://localhost:$port/" > /dev/null 2>&1; then
echo " $name: RUNNING (port :$port, PID stale — consider ncp kill)"
else
echo " $name: STOPPED"
fi
@@ -91,8 +167,8 @@ _status() {
kill_services() {
echo "Force-killing all Nexus processes..."
local ports=(8000 8001 5173)
local names=("SYNAPSE" "MEMORY" "INTERFACE")
local ports=(8000 8001 5173 11434)
local names=("SYNAPSE" "MEMORY" "INTERFACE" "OLLAMA")
for i in "${!ports[@]}"; do
port="${ports[$i]}"
name="${names[$i]}"
@@ -104,6 +180,8 @@ kill_services() {
done
pkill -9 -f "uvicorn synapse" 2>/dev/null || true
pkill -9 -f "npm run dev" 2>/dev/null || true
pkill -9 -f "vite --host" 2>/dev/null || true
pkill -9 -f "ollama serve" 2>/dev/null || true
rm -f "$PID_DIR"/*.pid
echo "Done."
}
@@ -112,57 +190,79 @@ kill_services() {
# START COMMANDS
# -----------------------------
start_ollama() {
# Ollama runs as its own `ollama serve` process, but the backend's
# OllamaManager owns model/GPU selection — so drive it through the backend
# endpoint (the same path the control panel uses) rather than launching the
# binary directly. Requires the backend to be up.
echo "Starting OLLAMA..."
if curl -s --max-time 30 -X POST http://localhost:8000/ollama/start > /dev/null 2>&1; then
echo "NEXUS OLLAMA STARTED"
else
echo " OLLAMA start request failed (backend not reachable on :8000)"
fi
}
start_memory() {
_launch "NEXUS MEMORY SERVICE" "$MEMORY_PID" "$MEMORY_LOG" "$NEXUS_ROOT" \
"$PYTHON" -m uvicorn synapse.memory.service:app --host 0.0.0.0 --port 8001 --reload
sleep 3
_check "NEXUS MEMORY SERVICE" "$MEMORY_PID" "$MEMORY_LOG"
_wait_for_port "NEXUS MEMORY SERVICE" 8001 "$MEMORY_PID" "$MEMORY_LOG"
}
start_backend() {
_launch "NEXUS BACKEND SERVICE" "$BACKEND_PID" "$BACKEND_LOG" "$NEXUS_ROOT" \
"$PYTHON" -m uvicorn synapse.main:sio_app --host 0.0.0.0 --port 8000 --reload
sleep 3
_check "NEXUS BACKEND SERVICE" "$BACKEND_PID" "$BACKEND_LOG"
_wait_for_port "NEXUS BACKEND SERVICE" 8000 "$BACKEND_PID" "$BACKEND_LOG"
start_ollama
}
start_frontend() {
_launch "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG" "$FRONTEND_DIR" \
npm run dev -- --host 0.0.0.0
sleep 5
_check "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG"
_wait_for_port "NEXUS FRONTEND SERVICE" 5173 "$FRONTEND_PID" "$FRONTEND_LOG"
}
start_all() {
# Memory + backend launch in parallel, then single wait for both
# Memory + backend launch in parallel, wait for both before starting frontend
_launch "NEXUS MEMORY SERVICE" "$MEMORY_PID" "$MEMORY_LOG" "$NEXUS_ROOT" \
"$PYTHON" -m uvicorn synapse.memory.service:app --host 0.0.0.0 --port 8001 --reload
_launch "NEXUS BACKEND SERVICE" "$BACKEND_PID" "$BACKEND_LOG" "$NEXUS_ROOT" \
"$PYTHON" -m uvicorn synapse.main:sio_app --host 0.0.0.0 --port 8000 --reload
sleep 3
_check "NEXUS MEMORY SERVICE" "$MEMORY_PID" "$MEMORY_LOG"
_check "NEXUS BACKEND SERVICE" "$BACKEND_PID" "$BACKEND_LOG"
# Frontend last (Vite takes a moment longer)
_wait_for_port "NEXUS MEMORY SERVICE" 8001 "$MEMORY_PID" "$MEMORY_LOG"
_wait_for_port "NEXUS BACKEND SERVICE" 8000 "$BACKEND_PID" "$BACKEND_LOG"
start_ollama
_launch "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG" "$FRONTEND_DIR" \
npm run dev -- --host 0.0.0.0
sleep 5
_check "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG"
_wait_for_port "NEXUS FRONTEND SERVICE" 5173 "$FRONTEND_PID" "$FRONTEND_LOG"
}
# -----------------------------
# STOP COMMANDS
# -----------------------------
stop_ollama() {
# Prefer the backend endpoint for a clean OllamaManager shutdown; if the
# backend is already down, kill `ollama serve` directly so it never lingers
# holding VRAM/RAM. Must run BEFORE the backend is torn down.
if curl -s --max-time 5 -X POST http://localhost:8000/ollama/stop > /dev/null 2>&1; then
echo "NEXUS OLLAMA STOPPED"
elif pgrep -f "ollama serve" > /dev/null 2>&1; then
pkill -TERM -f "ollama serve" 2>/dev/null
echo "NEXUS OLLAMA STOPPED (direct)"
fi
}
stop_memory() {
_stop "NEXUS MEMORY SERVICE" "$MEMORY_PID"
_stop_service "NEXUS MEMORY SERVICE" "$MEMORY_PID" 8001 "uvicorn synapse.memory"
}
stop_backend() {
_stop "NEXUS BACKEND SERVICE" "$BACKEND_PID"
stop_ollama
_stop_service "NEXUS BACKEND SERVICE" "$BACKEND_PID" 8000 "uvicorn synapse.main"
}
stop_frontend() {
_stop "NEXUS FRONTEND SERVICE" "$FRONTEND_PID"
_stop_service "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" 5173 "vite --host" "npm run dev"
}
stop_all() {
@@ -178,9 +278,19 @@ stop_all() {
status_services() {
echo "Nexus Service Status:"
echo
_status "Memory service" "$MEMORY_PID"
_status "Backend " "$BACKEND_PID"
_status "Frontend " "$FRONTEND_PID"
echo "Backend:"
_status "Synapse " "$BACKEND_PID" 8000
_status "Memory service" "$MEMORY_PID" 8001
echo
echo "Frontend:"
_status "Vite " "$FRONTEND_PID" 5173
echo
echo "Model server:"
if curl -s --max-time 1 "http://localhost:11434/api/tags" > /dev/null 2>&1; then
echo " Ollama : RUNNING (:11434)"
else
echo " Ollama : STOPPED"
fi
}
# -----------------------------
@@ -238,11 +348,31 @@ doctor() {
echo "Checking Node & npm..."
command -v node >/dev/null && echo " ✔ Node installed ($(node -v))" || echo " ✘ Node missing"
command -v npm >/dev/null && echo " ✔ npm installed ($(npm -v))" || echo " ✘ npm missing"
[ -d "$FRONTEND_DIR/node_modules" ] && echo " ✔ Frontend node_modules installed" || echo " ✘ Frontend node_modules missing (run: ncp update)"
echo
echo "Checking Uvicorn..."
"$PYTHON" -m uvicorn --version >/dev/null 2>&1 && echo " ✔ Uvicorn installed" || echo " ✘ Uvicorn missing"
echo
echo "Checking backend service..."
( cd "$NEXUS_ROOT" && "$PYTHON" -c "from synapse.main import sio_app" ) >/dev/null 2>&1 \
&& echo " ✔ Backend module importable" \
|| echo " ✘ Backend module failed to import"
echo
echo "Checking memory service..."
[ -d "$NEXUS_ROOT/synapse/memory" ] && echo " ✔ Memory module directory found" || echo " ✘ Missing memory module directory"
( cd "$NEXUS_ROOT" && "$PYTHON" -c "from synapse.memory.service import app" ) >/dev/null 2>&1 \
&& echo " ✔ Memory service module importable" \
|| echo " ✘ Memory service module failed to import"
[ -w "$NEXUS_ROOT/synapse/memory" ] && echo " ✔ Memory database directory writable" || echo " ✘ Memory database directory not writable"
echo
echo "Checking Ollama..."
[ -x "$OLLAMA_BIN" ] && echo " ✔ Ollama binary found ($OLLAMA_BIN)" || echo " ✘ Ollama binary missing at $OLLAMA_BIN"
[ -d "$OLLAMA_MODELS_DIR" ] && echo " ✔ Ollama models directory found ($OLLAMA_MODELS_DIR)" || echo " ✘ Ollama models directory missing at $OLLAMA_MODELS_DIR"
echo
status_services
}
@@ -257,28 +387,34 @@ show_help() {
echo "Usage: ncp <command> [options]"
echo
echo "Commands:"
echo " chat Open the interactive chat TUI"
echo " playbook Browse and edit playbooks"
echo " panel Launch the Nexus Control Panel"
echo
echo " chat <message> Send a message, stream the reply"
echo " memory list List memory facts"
echo " add <text> Add a fact (--section <name>)"
echo " rm <id> Delete a fact by id"
echo " playbook list List playbooks (* = active)"
echo " show <id> Print a playbook's goal + instructions"
echo " history [query] Recent conversations (optional keyword)"
echo
echo " start Start ALL Nexus services (memory + backend + frontend)"
echo " start --memory, -m Start only the memory service"
echo " start --frontend,-f Start only the frontend"
echo " start --backend, -b Start only the backend"
echo " --memory, -m Start only the memory service"
echo " --frontend,-f Start only the frontend"
echo " --backend, -b Start only the backend"
echo
echo " stop Stop ALL Nexus services"
echo " stop --memory, -m Stop only the memory service"
echo " stop --frontend,-f Stop only the frontend"
echo " stop --backend, -b Stop only the backend"
echo " --memory, -m Stop only the memory service"
echo " --frontend,-f Stop only the frontend"
echo " --backend, -b Stop only the backend"
echo
echo " kill Force-kill all Nexus processes by port (nuclear option)
refresh Restart all services"
echo " kill Force-kill all Nexus processes by port (nuclear option)"
echo " refresh Restart all services"
echo
echo " status Show service status"
echo " logs Show logs for all services"
echo " logs --memory, -m Memory service logs"
echo " logs --frontend,-f Frontend logs"
echo " logs --backend, -b Backend logs"
echo " --memory, -m Memory service logs"
echo " --frontend,-f Frontend logs"
echo " --backend, -b Backend logs"
echo
echo " doctor Run Nexus diagnostics"
echo " update Update Nexus dependencies"
@@ -307,19 +443,14 @@ update_nexus() {
cd "$NEXUS_ROOT" || exit 1
if [ -d ".git" ]; then
echo "Pulling latest changes from Git..."
git pull
else
echo "No Git repository found — skipping git pull."
fi
echo "Skipping git pull — update only refreshes dependencies."
echo
echo "Updating backend Python dependencies..."
if [ -f "amd_requirements.txt" ]; then
"$PYTHON" -m pip install -r amd_requirements.txt
if [ -f "requirements-amd.txt" ]; then
"$PYTHON" -m pip install -r requirements-amd.txt
else
echo "No amd_requirements.txt found."
echo "No requirements-amd.txt found."
fi
echo
@@ -347,35 +478,43 @@ ensure_router_reachable() {
return 0
fi
echo "Router unreachable. Activating WireGuard (wgs_client)..."
nmcli connection up wgs_client
echo "Waiting for VPN to establish..."
sleep 5
if ssh -q -o BatchMode=yes -o ConnectTimeout=10 router exit 2>/dev/null; then
echo "Router reachable via VPN."
return 0
# Fallback: bring up the WireGuard tunnel — but only on machines that
# actually have NetworkManager + the wgs_client connection (e.g. the roaming
# NexusOS laptop). On a directly-wired desktop or under WSL, nmcli/NM isn't
# usable, so skip the VPN step instead of dying with "permission denied".
if command -v nmcli >/dev/null 2>&1 && \
nmcli -t -f NAME connection show 2>/dev/null | grep -qx wgs_client; then
echo "Router unreachable. Activating WireGuard (wgs_client)..."
if nmcli connection up wgs_client; then
echo "Waiting for VPN to establish..."
sleep 5
if ssh -q -o BatchMode=yes -o ConnectTimeout=10 router exit 2>/dev/null; then
echo "Router reachable via VPN."
return 0
fi
fi
echo "Router still unreachable after VPN activation."
else
echo "Router unreachable, and no WireGuard fallback on this machine."
echo "Expected a direct LAN link to the server — check the 'router' SSH host and the connection."
fi
echo "Router still unreachable after VPN activation."
return 1
}
backup_nexus() {
ensure_router_reachable || return 1
echo "Backing up Nexus to router..."
bash "$NEXUS_ROOT/bin/backup-smart.sh"
bash "$NEXUS_ROOT/bin/backup.sh"
}
backup_nexus_full() {
ensure_router_reachable || return 1
echo "Full backup of Nexus to router..."
bash "$NEXUS_ROOT/bin/backup.sh"
bash "$NEXUS_ROOT/bin/backup.sh" full
}
restore_nexus() {
local script="$1"
local mode="$1"
ensure_router_reachable || return 1
echo "WARNING: This will overwrite local files with the remote backup."
printf "Continue? [y/N] "
@@ -384,7 +523,7 @@ restore_nexus() {
echo "Restore cancelled."
return
fi
bash "$NEXUS_ROOT/bin/$script"
bash "$NEXUS_ROOT/bin/restore.sh" $mode
}
# -----------------------------
@@ -493,8 +632,9 @@ shift
case "$subcommand" in
panel) python3 "$NEXUS_ROOT/management/controlpanel.py" ;;
chat) "$PYTHON" "$NEXUS_ROOT/management/nexus-chat.py" ;;
playbook) "$PYTHON" "$NEXUS_ROOT/management/nexus-playbook.py" ;;
chat|memory|playbook|history)
"$PYTHON" "$NEXUS_ROOT/management/nexus_api.py" "$subcommand" "$@" ;;
start)
case "$1" in
@@ -537,8 +677,8 @@ case "$subcommand" in
restore)
case "$1" in
-f|full) restore_nexus restore-full.sh ;;
"") restore_nexus restore.sh ;;
-f|full) restore_nexus full ;;
"") restore_nexus ;;
*) echo "Usage: ncp restore [-f|full]" ;;
esac
;;