feat: add portable NexusOS CLI and packaging

This commit is contained in:
2026-08-26 08:11:39 -05:00
parent 2ebe93b4f7
commit d579502a5b
21 changed files with 1624 additions and 94 deletions
+42 -15
View File
@@ -12,12 +12,16 @@ import os
import shutil
import subprocess
import urllib.request
from pathlib import Path
import psutil
try:
import psutil
except ImportError: # optional in the portable/Termux core install
psutil = None
from .nexus_config import PROJECT_ROOT, RUNTIME_DIR
from .nexus_config import FRONTEND_SOURCE_DIR, RUNTIME_DIR
FRONTEND_DIR = PROJECT_ROOT / "interface" / "web"
FRONTEND_DIR = FRONTEND_SOURCE_DIR
PID_FILE = RUNTIME_DIR / "pids" / "frontend.pid"
LOG_FILE = RUNTIME_DIR / "frontend.log"
PORT = 5173
@@ -41,12 +45,30 @@ def _is_ours(pid: int) -> bool:
# the tracked PID's own cmdline. Loosen to "vite" (matches once the tree
# gets that far) or "npm"+"dev" both present (matches the wrapper hop too).
try:
cmd = " ".join(psutil.Process(pid).cmdline())
if psutil is not None:
cmd = " ".join(psutil.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 "vite" in cmd or ("npm" in cmd and "dev" in cmd)
def _alive(pid: int | None) -> bool:
if pid is None:
return False
if psutil is not None:
return psutil.pid_exists(pid)
try:
os.kill(pid, 0)
return True
except (OSError, ValueError):
return False
def _http_up() -> bool:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1.5) as r:
@@ -60,7 +82,7 @@ def is_running() -> bool:
port actually answers (covers Vite started by another process, or a lost
PID file) - not the stricter pattern match `stop()` uses before killing."""
pid = _read_pid()
if pid is not None and psutil.pid_exists(pid):
if _alive(pid):
return True
return _http_up()
@@ -68,6 +90,8 @@ def is_running() -> bool:
def start() -> dict:
if is_running():
return {"status": "already_running"}
if not FRONTEND_DIR.is_dir():
return {"status": "unavailable", "detail": "Vite source is not included in wheel installs"}
npm = _npm()
if not npm:
return {"status": "error", "detail": "npm not found - install Node.js"}
@@ -92,17 +116,20 @@ def start() -> dict:
def stop() -> dict:
pid = _read_pid()
if pid is not None and psutil.pid_exists(pid) and _is_ours(pid):
if _alive(pid) and _is_ours(pid):
try:
proc = psutil.Process(pid)
# npm.cmd -> node -> vite is a multi-hop tree; kill it depth-first
# so the parent doesn't outlive its children as an orphaned shell.
for child in proc.children(recursive=True):
try:
child.terminate()
except Exception:
pass
proc.terminate()
if psutil is not None:
proc = psutil.Process(pid)
# npm.cmd -> node -> vite is a multi-hop tree; kill it depth-first
# so the parent doesn't outlive its children as an orphaned shell.
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
PID_FILE.unlink(missing_ok=True)