feat: add portable NexusOS CLI and packaging

This commit is contained in:
2026-08-20 02:00:52 -05:00
parent 4269a2f44f
commit 646af18c7d
21 changed files with 1623 additions and 97 deletions
+93 -32
View File
@@ -27,15 +27,20 @@ import time
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PID_DIR = ROOT / "runtime" / "pids"
LOG_DIR = ROOT / "runtime"
FRONTEND_DIR = ROOT / "interface" / "web"
from synapse.nexus_config import SOURCE_CHECKOUT, settings
ROOT = settings.project_root
PID_DIR = settings.runtime_dir / "pids"
LOG_DIR = settings.runtime_dir
FRONTEND_DIR = settings.frontend_source_dir
OLLAMA_BIN = ROOT / "ollama" / "bin" / ("ollama.exe" if os.name == "nt" else "ollama")
OLLAMA_MODELS_DIR = ROOT / "models"
OLLAMA_MODELS_DIR = settings.models_dir
WINDOWS = os.name == "nt"
PYTHON = ROOT / "Promethean" / ("Scripts/python.exe" if WINDOWS else "bin/python3")
_VENV_PYTHON = ROOT / "Promethean" / ("Scripts/python.exe" if WINDOWS else "bin/python3")
PYTHON = Path(os.getenv("NEXUS_PYTHON", "")) if os.getenv("NEXUS_PYTHON") else (
_VENV_PYTHON if _VENV_PYTHON.exists() else Path(sys.executable)
)
PID_DIR.mkdir(parents=True, exist_ok=True)
LOG_DIR.mkdir(parents=True, exist_ok=True)
@@ -92,7 +97,7 @@ def _psutil():
import psutil
return psutil
except ImportError:
sys.exit("psutil is missing - run ./install.sh (or install-windows.ps1) to rebuild the venv.")
return None
# -- services ------------------------------------------------------------------
@@ -115,7 +120,7 @@ class Service:
# Bind loopback by default: the backend/memory REST APIs are unauthenticated, so
# binding 0.0.0.0 handed the full admin+data plane to any host on the LAN. Set
# NEXUS_BIND_HOST=0.0.0.0 to opt into LAN exposure once real auth is in place.
BIND_HOST = os.environ.get("NEXUS_BIND_HOST", "127.0.0.1")
BIND_HOST = settings.bind_host
def _uvicorn(app: str, port: int):
@@ -131,12 +136,12 @@ def _uvicorn(app: str, port: int):
SERVICES = {
"memory": Service("memory", "NEXUS MEMORY SERVICE", 8001, ROOT,
"memory": Service("memory", "NEXUS MEMORY SERVICE", settings.memory_port, settings.state_dir,
["uvicorn synapse.memory"],
lambda: _uvicorn("synapse.memory.service:app", 8001)),
"backend": Service("backend", "NEXUS BACKEND SERVICE", 8000, ROOT,
lambda: _uvicorn("synapse.memory.service:app", settings.memory_port)),
"backend": Service("backend", "NEXUS BACKEND SERVICE", settings.backend_port, settings.state_dir,
["uvicorn synapse.main"],
lambda: _uvicorn("synapse.main:sio_app", 8000)),
lambda: _uvicorn("synapse.main:sio_app", settings.backend_port)),
"frontend": Service("frontend", "NEXUS FRONTEND SERVICE", 5173, FRONTEND_DIR,
["vite --host", "npm run dev"],
lambda: [npm(), "run", "dev", "--", "--host", "0.0.0.0"]),
@@ -152,7 +157,15 @@ def read_pid(svc: Service):
def alive(pid) -> bool:
ps = _psutil()
return pid is not None and ps.pid_exists(pid)
if pid is None:
return False
if ps is not None:
return ps.pid_exists(pid)
try:
os.kill(pid, 0)
return True
except (OSError, ValueError):
return False
def pid_is_ours(pid, patterns) -> bool:
@@ -162,7 +175,13 @@ def pid_is_ours(pid, patterns) -> bool:
is not enough. TERMing a recycled PID can log the user out."""
ps = _psutil()
try:
cmd = " ".join(ps.Process(pid).cmdline())
if ps is not None:
cmd = " ".join(ps.Process(pid).cmdline())
elif os.name != "nt":
cmd = (Path(f"/proc/{pid}/cmdline").read_bytes()
.replace(b"\0", b" ").decode(errors="replace"))
else:
return False
except Exception:
return False
return any(p in cmd for p in patterns)
@@ -249,6 +268,20 @@ def kill_matching(patterns, force=False) -> int:
ps = _psutil()
me = os.getpid()
hit = 0
if ps is None:
if os.name == "nt":
return 0
for entry in Path("/proc").iterdir():
if not entry.name.isdigit() or int(entry.name) == me:
continue
try:
cmd = (entry / "cmdline").read_bytes().replace(b"\0", b" ").decode(errors="replace")
if any(pattern in cmd for pattern in patterns):
os.kill(int(entry.name), 9 if force else 15)
hit += 1
except (OSError, ValueError):
pass
return hit
for proc in ps.process_iter(["pid", "cmdline"]):
if proc.info["pid"] == me:
continue
@@ -266,6 +299,8 @@ def kill_port(port: int) -> bool:
"""Whatever holds the port IS the service - this is the backstop that makes
stop reliable regardless of process-tree shape or PID-file accuracy."""
ps = _psutil()
if ps is None:
return False
killed = False
try:
conns = ps.net_connections(kind="inet")
@@ -289,13 +324,16 @@ def stop_service(svc: Service) -> bool:
if alive(pid) and pid_is_ours(pid, svc.patterns):
ps = _psutil()
try:
proc = ps.Process(pid)
for child in proc.children(recursive=True):
try:
child.terminate()
except Exception:
pass
proc.terminate()
if ps is not None:
proc = ps.Process(pid)
for child in proc.children(recursive=True):
try:
child.terminate()
except Exception:
pass
proc.terminate()
elif os.name != "nt":
os.kill(pid, 15)
except Exception:
pass
svc.pid_file.unlink(missing_ok=True)
@@ -326,8 +364,15 @@ def start_ollama(background: bool = False) -> None:
background=False (`ncp start --ai`): blocks until the model is warmed - weights
read off disk into RAM/VRAM, routinely about a minute - so "started" means the
AI can actually answer."""
if not settings.manage_ollama:
running = http_ok(settings.ollama_host.rstrip("/") + "/api/tags", timeout=3)
print(
f"REMOTE OLLAMA {'REACHABLE' if running else 'UNREACHABLE'} "
f"({settings.ollama_host})"
)
return
print("Starting OLLAMA...")
url = "http://localhost:8000/ollama/start" + ("?background=true" if background else "")
url = settings.api_url + "/ollama/start" + ("?background=true" if background else "")
req = urllib.request.Request(url, method="POST")
try:
urllib.request.urlopen(req, timeout=180).read(1)
@@ -340,7 +385,10 @@ def stop_ollama() -> None:
"""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."""
req = urllib.request.Request("http://localhost:8000/ollama/stop", method="POST")
if not settings.manage_ollama:
print(f"REMOTE OLLAMA IS EXTERNALLY MANAGED ({settings.ollama_host})")
return
req = urllib.request.Request(settings.api_url + "/ollama/stop", method="POST")
try:
urllib.request.urlopen(req, timeout=5).read(1)
print("NEXUS OLLAMA STOPPED")
@@ -363,6 +411,9 @@ def cmd_start(target) -> None:
launch(SERVICES["backend"]); wait_for_port(SERVICES["backend"])
start_ollama()
elif target in ("--frontend", "-f"):
if not SOURCE_CHECKOUT:
print("Vite source is unavailable in wheel installs; the backend serves the bundled UI.")
return
launch(SERVICES["frontend"]); wait_for_port(SERVICES["frontend"])
elif target in ("--ai", "-a"):
start_ollama()
@@ -387,7 +438,7 @@ def cmd_start(target) -> None:
wait_for_port(SERVICES["backend"])
t_services = time.perf_counter()
start_ollama(background=True)
if not WINDOWS:
if SOURCE_CHECKOUT and not WINDOWS:
launch(SERVICES["frontend"])
t_bg = time.perf_counter()
print("\nBoot timing:")
@@ -405,6 +456,8 @@ def cmd_stop(target) -> None:
stop_ollama(); stop_service(SERVICES["backend"])
elif target in ("--frontend", "-f"):
stop_service(SERVICES["frontend"])
elif target in ("--ai", "-a"):
stop_ollama()
elif target in (None, "", "all"):
stop_service(SERVICES["memory"])
stop_ollama()
@@ -416,14 +469,21 @@ def cmd_stop(target) -> None:
def cmd_kill() -> None:
print("Force-killing all Nexus processes...")
for port, name in ((8000, "SYNAPSE"), (8001, "MEMORY"),
(5173, "INTERFACE"), (11434, "OLLAMA")):
targets = [
(settings.backend_port, "SYNAPSE"),
(settings.memory_port, "MEMORY"),
(5173, "INTERFACE"),
]
patterns = ["uvicorn synapse", "npm run dev", "vite --host"]
if settings.manage_ollama:
targets.append((11434, "OLLAMA"))
patterns.append("ollama serve")
for port, name in targets:
if kill_port(port):
print(f" KILLED: {name} (:{port})")
else:
print(f" NOT RUNNING: {name} (:{port})")
kill_matching(["uvicorn synapse", "npm run dev", "vite --host", "ollama serve"],
force=True)
kill_matching(patterns, force=True)
for pid_file in PID_DIR.glob("*.pid"):
pid_file.unlink(missing_ok=True)
print("Done.")
@@ -447,8 +507,9 @@ def cmd_status() -> None:
print("\nFrontend:")
one("Vite ", SERVICES["frontend"])
print("\nModel server:")
running = http_ok("http://localhost:11434/api/tags")
print(f" Ollama : {'RUNNING (:11434)' if running else 'STOPPED'}")
running = http_ok(settings.ollama_host.rstrip("/") + "/api/tags")
mode = "remote" if not settings.manage_ollama else "local"
print(f" Ollama ({mode}) : {'RUNNING' if running else 'STOPPED'} ({settings.ollama_host})")
def _tail(path: Path, n: int) -> None:
@@ -670,10 +731,10 @@ MODEL_CATALOG = [
def cmd_models(action, name) -> None:
if action == "list":
if not http_ok("http://localhost:11434/api/tags"):
if not http_ok(settings.ollama_host.rstrip("/") + "/api/tags"):
print("Ollama is not running. Start the backend first with: ncp start -b")
return
raw = urllib.request.urlopen("http://localhost:11434/api/tags", timeout=5).read()
raw = urllib.request.urlopen(settings.ollama_host.rstrip("/") + "/api/tags", timeout=5).read()
models = json.loads(raw).get("models", [])
print("Installed models:\n")
if not models: