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:
Executable
+314
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env bash
|
||||
# NexusOS WSL bootstrap — called by install-windows.ps1 after the repo is copied in.
|
||||
# Safe to re-run; skips steps that are already done.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$NEXUS_ROOT"
|
||||
|
||||
step() { echo ""; echo "==> $*"; }
|
||||
ok() { echo " ok: $*"; }
|
||||
warn() { echo " warn: $*"; }
|
||||
|
||||
# ── System packages ───────────────────────────────────────────────────────────
|
||||
|
||||
step "System packages"
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
python3 python3-venv python3-pip python3-tk \
|
||||
curl wget git build-essential \
|
||||
libsqlite3-dev rsync
|
||||
ok "System packages ready"
|
||||
|
||||
# ── Node.js (via NodeSource LTS) ──────────────────────────────────────────────
|
||||
|
||||
step "Node.js"
|
||||
if command -v node &>/dev/null; then
|
||||
ok "Already installed: $(node --version)"
|
||||
else
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - 2>/dev/null
|
||||
sudo apt-get install -y -qq nodejs
|
||||
ok "Installed: $(node --version)"
|
||||
fi
|
||||
|
||||
# ── Python venv ───────────────────────────────────────────────────────────────
|
||||
|
||||
step "Python venv (Promethean)"
|
||||
if [ ! -d "$NEXUS_ROOT/Promethean" ]; then
|
||||
python3 -m venv "$NEXUS_ROOT/Promethean"
|
||||
ok "Created"
|
||||
else
|
||||
ok "Already exists"
|
||||
fi
|
||||
|
||||
source "$NEXUS_ROOT/Promethean/bin/activate"
|
||||
pip install --upgrade pip -q
|
||||
|
||||
step "Python dependencies"
|
||||
pip install -r "$NEXUS_ROOT/requirements-wsl.txt" -q
|
||||
ok "Installed"
|
||||
|
||||
# ── Ollama ────────────────────────────────────────────────────────────────────
|
||||
|
||||
step "Ollama"
|
||||
mkdir -p "$NEXUS_ROOT/runtime/logs"
|
||||
OLLAMA_BIN="$NEXUS_ROOT/ollama/bin/ollama"
|
||||
DEFAULT_MODEL="${NEXUS_DEFAULT_MODEL:-qwen2.5:3b}"
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) ;;
|
||||
*)
|
||||
echo "ERROR: bundled Ollama supports x86-64 WSL only (detected $(uname -m))." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -x "$OLLAMA_BIN" ]; then
|
||||
chmod +x "$OLLAMA_BIN" 2>/dev/null || true
|
||||
fi
|
||||
if [ ! -x "$OLLAMA_BIN" ] || ! "$OLLAMA_BIN" --version &>/dev/null; then
|
||||
echo "ERROR: bundled Ollama could not run in this WSL distro." >&2
|
||||
echo " Expected a Linux x86-64 executable at: $OLLAMA_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
ok "Binary OK: $("$OLLAMA_BIN" --version)"
|
||||
|
||||
# A clean checkout has no model files. Provision one so the first launch can chat.
|
||||
OLLAMA_HOST="127.0.0.1:11434"
|
||||
OLLAMA_MODELS="$NEXUS_ROOT/models"
|
||||
OLLAMA_HOST="$OLLAMA_HOST" OLLAMA_MODELS="$OLLAMA_MODELS" \
|
||||
"$OLLAMA_BIN" list 2>/dev/null | awk -v model="$DEFAULT_MODEL" \
|
||||
'NR > 1 && $1 == model { found = 1 } END { exit !found }' \
|
||||
|| {
|
||||
ollama_pid=""
|
||||
if ! curl -fsS "http://127.0.0.1:11434/api/tags" >/dev/null 2>&1; then
|
||||
OLLAMA_HOST="$OLLAMA_HOST" OLLAMA_MODELS="$NEXUS_ROOT/models" \
|
||||
"$OLLAMA_BIN" serve >>"$NEXUS_ROOT/runtime/logs/ollama.log" 2>&1 &
|
||||
ollama_pid=$!
|
||||
for _ in {1..30}; do
|
||||
curl -fsS "http://127.0.0.1:11434/api/tags" >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
if ! curl -fsS "http://127.0.0.1:11434/api/tags" >/dev/null 2>&1; then
|
||||
[ -n "$ollama_pid" ] && kill "$ollama_pid" 2>/dev/null || true
|
||||
echo "ERROR: Ollama did not start; see runtime/logs/ollama.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! OLLAMA_HOST="$OLLAMA_HOST" OLLAMA_MODELS="$OLLAMA_MODELS" \
|
||||
"$OLLAMA_BIN" list 2>/dev/null | awk -v model="$DEFAULT_MODEL" \
|
||||
'NR > 1 && $1 == model { found = 1 } END { exit !found }'; then
|
||||
echo " pulling default model: $DEFAULT_MODEL"
|
||||
if ! OLLAMA_HOST="$OLLAMA_HOST" OLLAMA_MODELS="$OLLAMA_MODELS" \
|
||||
"$OLLAMA_BIN" pull "$DEFAULT_MODEL"; then
|
||||
[ -n "$ollama_pid" ] && kill "$ollama_pid" 2>/dev/null || true
|
||||
echo "ERROR: Could not pull default model $DEFAULT_MODEL." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
[ -n "$ollama_pid" ] && kill "$ollama_pid" 2>/dev/null || true
|
||||
ok "Model ready: $DEFAULT_MODEL"
|
||||
}
|
||||
|
||||
# ── Frontend npm deps ─────────────────────────────────────────────────────────
|
||||
|
||||
step "Frontend dependencies"
|
||||
cd "$NEXUS_ROOT/interface/web"
|
||||
npm install --silent
|
||||
ok "Installed"
|
||||
cd "$NEXUS_ROOT"
|
||||
|
||||
# ── Runtime dirs ─────────────────────────────────────────────────────────────
|
||||
|
||||
step "Runtime directories"
|
||||
mkdir -p "$NEXUS_ROOT/runtime/pids" "$NEXUS_ROOT/runtime/logs"
|
||||
ok "Ready"
|
||||
|
||||
# ── Shell alias (ncp) ─────────────────────────────────────────────────────────
|
||||
|
||||
step "Shell alias"
|
||||
if ! grep -q "nexus-core/management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
|
||||
cat >> "$HOME/.bashrc" << 'EOF'
|
||||
|
||||
# NexusOS
|
||||
ncp() { ~/nexus-core/management/nexus-cli.sh "$@"; }
|
||||
EOF
|
||||
ok "ncp alias added to ~/.bashrc"
|
||||
else
|
||||
ok "ncp already in ~/.bashrc"
|
||||
fi
|
||||
|
||||
step "Promethean command"
|
||||
PROMETHEAN_RC="$NEXUS_ROOT/.promethean_bashrc"
|
||||
if [ ! -f "$PROMETHEAN_RC" ]; then
|
||||
cat > "$PROMETHEAN_RC" << 'EOF'
|
||||
# Promethean Terminal shell init
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc
|
||||
export VIRTUAL_ENV_DISABLE_PROMPT=1
|
||||
if [[ -f "$HOME/nexus-core/Promethean/bin/activate" ]]; then
|
||||
source "$HOME/nexus-core/Promethean/bin/activate"
|
||||
fi
|
||||
export PS1='\[\e[0;35m\](Promethean)\[\e[0m\] ${debian_chroot:+($debian_chroot)}\[\e[0;37m\]\u@\h\[\e[0m\]:\[\e[1;32m\]\w\[\e[0m\]\$ '
|
||||
printf '\e]0;Promethean Terminal\a'
|
||||
EOF
|
||||
ok "Promethean environment init created"
|
||||
else
|
||||
ok "Promethean environment init already exists"
|
||||
fi
|
||||
if ! grep -qF "alias promethean='source ~/nexus-core/.promethean_bashrc'" "$HOME/.bashrc" 2>/dev/null; then
|
||||
cat >> "$HOME/.bashrc" << 'EOF'
|
||||
|
||||
# Promethean
|
||||
alias promethean='source ~/nexus-core/.promethean_bashrc'
|
||||
EOF
|
||||
ok "promethean command added to ~/.bashrc"
|
||||
else
|
||||
ok "promethean command already in ~/.bashrc"
|
||||
fi
|
||||
|
||||
# ── Windows Terminal profile + desktop shortcut ───────────────────────────────
|
||||
|
||||
setup_windows() {
|
||||
local win_user distro icon_src icon_win_dir icon_win_path
|
||||
|
||||
win_user=$(powershell.exe -command '$env:USERNAME' 2>/dev/null | tr -d '\r\n')
|
||||
distro="${WSL_DISTRO_NAME:-$(uname -n)}"
|
||||
|
||||
if [ -z "$win_user" ] || [ ! -d "/mnt/c/Users/$win_user" ]; then
|
||||
warn "Cannot locate Windows user directory — skipping Windows Terminal setup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Copy app icon into Windows filesystem so WT can display it
|
||||
icon_src="$NEXUS_ROOT/assets/n-small.png"
|
||||
icon_win_dir="/mnt/c/Users/$win_user/AppData/Local/NexusOS"
|
||||
mkdir -p "$icon_win_dir"
|
||||
[ -f "$icon_src" ] && cp "$icon_src" "$icon_win_dir/nexus.png"
|
||||
icon_win_path="C:\\Users\\$win_user\\AppData\\Local\\NexusOS\\nexus.png"
|
||||
|
||||
# Init file so the WT session sources nvm + ncp
|
||||
cat > "$HOME/.nexus_init" << 'EOF'
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc
|
||||
EOF
|
||||
|
||||
# Patch Windows Terminal settings.json via Python (handles JSONC safely)
|
||||
python3 - "$win_user" "$distro" "$icon_win_path" << 'PYEOF'
|
||||
import json, re, uuid, sys
|
||||
from pathlib import Path
|
||||
|
||||
win_user, distro, icon_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
candidates = [
|
||||
f"/mnt/c/Users/{win_user}/AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json",
|
||||
f"/mnt/c/Users/{win_user}/AppData/Local/Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json",
|
||||
]
|
||||
|
||||
settings_path = next((Path(p) for p in candidates if Path(p).exists()), None)
|
||||
if not settings_path:
|
||||
print(" warn: Windows Terminal not found — skipping profile")
|
||||
sys.exit(0)
|
||||
|
||||
def strip_jsonc(text):
|
||||
output = []
|
||||
in_string = False
|
||||
escaped = False
|
||||
in_line_comment = False
|
||||
in_block_comment = False
|
||||
index = 0
|
||||
while index < len(text):
|
||||
char = text[index]
|
||||
next_char = text[index + 1] if index + 1 < len(text) else ""
|
||||
if in_line_comment:
|
||||
if char == "\n":
|
||||
in_line_comment = False
|
||||
output.append(char)
|
||||
elif in_block_comment:
|
||||
if char == "*" and next_char == "/":
|
||||
in_block_comment = False
|
||||
index += 1
|
||||
elif in_string:
|
||||
output.append(char)
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif char == "\\":
|
||||
escaped = True
|
||||
elif char == '"':
|
||||
in_string = False
|
||||
elif char == '"':
|
||||
in_string = True
|
||||
output.append(char)
|
||||
elif char == "/" and next_char == "/":
|
||||
in_line_comment = True
|
||||
index += 1
|
||||
elif char == "/" and next_char == "*":
|
||||
in_block_comment = True
|
||||
index += 1
|
||||
else:
|
||||
output.append(char)
|
||||
index += 1
|
||||
return "".join(output)
|
||||
|
||||
try:
|
||||
with open(settings_path, encoding="utf-8") as f:
|
||||
settings_text = strip_jsonc(f.read())
|
||||
settings = json.loads(re.sub(r",(\s*[}\]])", r"\1", settings_text))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
print(f" warn: Could not read Windows Terminal settings: {error}")
|
||||
sys.exit(0)
|
||||
|
||||
profiles = settings.setdefault("profiles", {}).setdefault("list", [])
|
||||
if any(p.get("name") == "NexusOS" for p in profiles):
|
||||
print(" ok: NexusOS profile already in Windows Terminal")
|
||||
sys.exit(0)
|
||||
|
||||
profiles.insert(0, {
|
||||
"guid": "{" + str(uuid.uuid4()) + "}",
|
||||
"name": "NexusOS",
|
||||
"commandline": f"wsl.exe -d {distro} bash --init-file ~/.nexus_init",
|
||||
"startingDirectory": "~",
|
||||
"icon": icon_path,
|
||||
})
|
||||
|
||||
try:
|
||||
with open(settings_path, "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, indent=4)
|
||||
except OSError as error:
|
||||
print(f" warn: Could not update Windows Terminal settings: {error}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f" ok: NexusOS profile added to Windows Terminal")
|
||||
PYEOF
|
||||
|
||||
# Desktop shortcut — opens WT to the NexusOS profile and launches Nexus
|
||||
powershell.exe -command "
|
||||
\$ws = New-Object -ComObject WScript.Shell
|
||||
\$s = \$ws.CreateShortcut(\"\$env:USERPROFILE\Desktop\NexusOS.lnk\")
|
||||
\$s.TargetPath = 'wt.exe'
|
||||
\$s.Arguments = '--profile NexusOS -- bash -c \"cd ~/nexus-core && ./launch_nexus.sh ; exec bash\"'
|
||||
\$s.IconLocation = 'wt.exe,0'
|
||||
\$s.Save()
|
||||
" 2>/dev/null && ok "Desktop shortcut created" || warn "Could not create desktop shortcut"
|
||||
}
|
||||
|
||||
step "Windows integration"
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
setup_windows
|
||||
else
|
||||
ok "Not running in WSL — skipping Windows Terminal setup"
|
||||
fi
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " NexusOS bootstrap complete!"
|
||||
echo ""
|
||||
echo " Launch: cd ~/nexus-core && ./launch_nexus.sh"
|
||||
echo " Frontend: http://localhost:5173"
|
||||
echo " Backend: http://localhost:8000"
|
||||
echo ""
|
||||
echo " Or use the NexusOS shortcut on your Windows desktop."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Reference in New Issue
Block a user