forked from enderofwings/NexusOS
feat: put ncp on PATH on both platforms; rename WSL reqs to Windows
Ports the ncp PATH work from upstream. Registering ncp as a shell-profile function failed three ways on Windows: the default Restricted execution policy blocks the profile itself, profiles don't exist outside PowerShell (cmd, Win+R, Task Scheduler), and the self-elevating installer writes the admin's profile. Windows now ships management/ncp.cmd and the installer appends management\ to the Machine PATH via [Environment]::SetEnvironmentVariable -- never setx, which truncates PATH at 1024 chars. A .cmd is exempt from the execution policy. Linux symlinks /usr/local/bin/ncp -> management/nexus-cli.sh, falling back to the old .bashrc function when sudo is unavailable. Also renames requirements-wsl.txt to requirements-windows.txt and purges stale WSL references, including vite.config.js's dev-server comment and controlpanel.py's "Check WSLg." error string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ start --ai`), not on backend startup.
|
|||||||
**Windows (recommended):**
|
**Windows (recommended):**
|
||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1 # one-time native install
|
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1 # one-time native install
|
||||||
.\launch_nexus.ps1 # memory :8001 + backend :8000
|
ncp web # memory :8001 + backend :8000 + UI
|
||||||
```
|
```
|
||||||
|
|
||||||
**Linux full stack (dev):**
|
**Linux full stack (dev):**
|
||||||
@@ -80,7 +80,7 @@ cd interface/web && npm run build
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Python venv
|
### Python venv
|
||||||
All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. Dependencies are layered: `requirements-base.txt` holds the GPU-agnostic core, and a thin overlay pins the right PyTorch build for the target — `requirements-amd.txt` (ROCm), `requirements-nvidia.txt` (CUDA, generated by `bin/gen-nvidia-reqs.py`), or `requirements-wsl.txt` (CPU-only). `bin/install.sh` selects NVIDIA, AMD, or CPU/WSL requirements from the host.
|
All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. Dependencies are layered: `requirements-base.txt` holds the GPU-agnostic core, and a thin overlay pins the right PyTorch build for the target — `requirements-amd.txt` (ROCm), `requirements-nvidia.txt` (CUDA, generated by `bin/gen-nvidia-reqs.py`), or `requirements-windows.txt` (CPU-only). `bin/sync.py` (`requirements()`) selects NVIDIA, AMD, or CPU/Windows requirements from the host.
|
||||||
|
|
||||||
### Synapse Backend (`synapse/`)
|
### Synapse Backend (`synapse/`)
|
||||||
FastAPI app at `synapse/main.py`. Key responsibilities:
|
FastAPI app at `synapse/main.py`. Key responsibilities:
|
||||||
@@ -125,4 +125,4 @@ Most data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory
|
|||||||
| Python dependencies (base) | `requirements-base.txt` |
|
| Python dependencies (base) | `requirements-base.txt` |
|
||||||
| Python dependencies (AMD/ROCm) | `requirements-amd.txt` |
|
| Python dependencies (AMD/ROCm) | `requirements-amd.txt` |
|
||||||
| Python dependencies (NVIDIA/CUDA) | `requirements-nvidia.txt` (generated by `bin/gen-nvidia-reqs.py`) |
|
| Python dependencies (NVIDIA/CUDA) | `requirements-nvidia.txt` (generated by `bin/gen-nvidia-reqs.py`) |
|
||||||
| Python dependencies (WSL/CPU) | `requirements-wsl.txt` |
|
| Python dependencies (Windows/CPU) | `requirements-windows.txt` |
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ memory service, and a React frontend. No external AI provider is called.
|
|||||||
NexusOS ("Nexus") is a self-hosted assistant you actually own. All inference
|
NexusOS ("Nexus") is a self-hosted assistant you actually own. All inference
|
||||||
runs through a **locally bundled Ollama** on `localhost`; conversations, facts,
|
runs through a **locally bundled Ollama** on `localhost`; conversations, facts,
|
||||||
and settings live in local SQLite. It ships with desktop branding (XFCE theme,
|
and settings live in local SQLite. It ships with desktop branding (XFCE theme,
|
||||||
icons, boot splash) so it can be run as a full assistant environment, not just
|
icons, boot splash) so it can be run as a full assistant environment on Linux,
|
||||||
a web app.
|
not just a web app.
|
||||||
|
|
||||||
- **Chat** — streaming responses from local Ollama models (SSE).
|
- **Chat** — streaming responses from local Ollama models (SSE).
|
||||||
- **Persistent memory** — a dedicated service auto-extracts durable facts from
|
- **Persistent memory** — a dedicated service auto-extracts durable facts from
|
||||||
@@ -47,26 +47,21 @@ cd nexus-core
|
|||||||
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
|
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Execution policy:** Windows blocks unsigned `.ps1` scripts by default, so
|
Then double-click the **NexusOS** desktop icon, or launch from a **new** shell
|
||||||
> run them with `-ExecutionPolicy Bypass` as shown (a one-run override — nothing
|
(PATH is read at process start, so already-open windows won't have `ncp` yet):
|
||||||
> permanent). Double-clicking `install-windows.ps1` or running `.\install-windows.ps1`
|
|
||||||
> bare will fail with *"running scripts is disabled on this system"*.
|
|
||||||
> The installer self-elevates (a UAC prompt will appear).
|
|
||||||
>
|
|
||||||
> To allow scripts persistently instead (then you can run `.\...ps1` directly):
|
|
||||||
> ```powershell
|
|
||||||
> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
|
||||||
> ```
|
|
||||||
|
|
||||||
Then double-click the **NexusOS** desktop icon (the shortcut already passes the
|
|
||||||
bypass), or launch from a shell with:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File .\launch_nexus.ps1
|
ncp web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> The installer needs `-ExecutionPolicy Bypass` because Windows blocks unsigned
|
||||||
|
> `.ps1` by default; it's a one-run override, nothing permanent. It self-elevates,
|
||||||
|
> so a UAC prompt appears. `ncp` itself is `management\ncp.cmd` on the machine
|
||||||
|
> PATH — a `.cmd`, so no execution-policy change is ever needed, and it works
|
||||||
|
> from cmd and Task Scheduler as well as PowerShell.
|
||||||
|
|
||||||
The app opens at `:8000`; click **Start AI** to launch Ollama. The installer
|
The app opens at `:8000`; click **Start AI** to launch Ollama. The installer
|
||||||
uses `requirements-wsl.txt` (CPU-only, pure-Python — no ML stack, since Ollama
|
uses `requirements-windows.txt` (CPU-only, pure-Python — no ML stack, since Ollama
|
||||||
does all inference over HTTP).
|
does all inference over HTTP).
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
@@ -77,12 +72,13 @@ does all inference over HTTP).
|
|||||||
./install.sh
|
./install.sh
|
||||||
|
|
||||||
# 2. Launch (memory :8001, backend :8000 — backend also serves the built UI)
|
# 2. Launch (memory :8001, backend :8000 — backend also serves the built UI)
|
||||||
./launch_nexus.sh
|
# The install symlinks ncp into /usr/local/bin (sudo); open a new shell first.
|
||||||
|
ncp web
|
||||||
```
|
```
|
||||||
|
|
||||||
Python deps are layered: `requirements-base.txt` (GPU-agnostic core) plus one
|
Python deps are layered: `requirements-base.txt` (GPU-agnostic core) plus one
|
||||||
GPU overlay — `requirements-amd.txt` (ROCm) or `requirements-nvidia.txt` (CUDA).
|
GPU overlay — `requirements-amd.txt` (ROCm) or `requirements-nvidia.txt` (CUDA).
|
||||||
`requirements-wsl.txt` is the standalone CPU-only runtime (no base overlay).
|
`requirements-windows.txt` is the standalone CPU-only runtime (no base overlay).
|
||||||
`bin/sync.py` 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
|
`./install.sh` is also the update path — re-run it any time to pull the latest
|
||||||
@@ -115,12 +111,14 @@ ncp chat "<message>" # stream a reply
|
|||||||
ncp memory list | add <text> | rm <id>
|
ncp memory list | add <text> | rm <id>
|
||||||
ncp playbook list | show <id> # first playbook (*) = active system prompt
|
ncp playbook list | show <id> # first playbook (*) = active system prompt
|
||||||
ncp history [query] # recent conversations
|
ncp history [query] # recent conversations
|
||||||
|
ncp doctor # diagnostics: venv, Node, imports, Ollama, status
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
| Component | Location | Role |
|
| Component | Location | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| **Promethean** (venv) | `Promethean/` | The Python venv all backend code runs in — `source Promethean/bin/activate` (Linux) / `Promethean\Scripts\python.exe` (Windows). Keeps deps out of the system Python. |
|
||||||
| **Synapse** (backend) | `synapse/` | FastAPI app. `/chat/stream`, `/playbooks`, `/memory`, `/models`, `/conversations`, `/settings`, `/ollama`, `/icons`. Assembles the system prompt: active playbook → reference playbooks → memory facts → relevant past snippets. |
|
| **Synapse** (backend) | `synapse/` | FastAPI app. `/chat/stream`, `/playbooks`, `/memory`, `/models`, `/conversations`, `/settings`, `/ollama`, `/icons`. Assembles the system prompt: active playbook → reference playbooks → memory facts → relevant past snippets. |
|
||||||
| **Memory service** | `synapse/memory/` | Separate FastAPI app (:8001). `/memories/extract` uses an Ollama prompt to decide what to persist. Shares the SQLite DB with the backend. |
|
| **Memory service** | `synapse/memory/` | Separate FastAPI app (:8001). `/memories/extract` uses an Ollama prompt to decide what to persist. Shares the SQLite DB with the backend. |
|
||||||
| **Playbooks** | `synapse/playbooks/` + `data/playbooks/` | Ordered `{id}.yaml` records managed by `PlaybookManager`. |
|
| **Playbooks** | `synapse/playbooks/` + `data/playbooks/` | Ordered `{id}.yaml` records managed by `PlaybookManager`. |
|
||||||
@@ -142,6 +140,7 @@ management/ nexus-cli.sh, ncp API client, control panel, desktop theme
|
|||||||
bin/ install, backup/restore, panel + provisioning scripts
|
bin/ install, backup/restore, panel + provisioning scripts
|
||||||
assets/ branding: icons, boot splash, XFCE/GTK theme
|
assets/ branding: icons, boot splash, XFCE/GTK theme
|
||||||
data/playbooks/ active playbook YAML
|
data/playbooks/ active playbook YAML
|
||||||
|
Promethean/ Python venv (gitignored, built by the installer)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -151,7 +150,7 @@ data/playbooks/ active playbook YAML
|
|||||||
| Ollama host | `OLLAMA_HOST` env (default `http://127.0.0.1:11434`) |
|
| Ollama host | `OLLAMA_HOST` env (default `http://127.0.0.1:11434`) |
|
||||||
| Filesystem paths | `synapse/nexus_config.py` |
|
| Filesystem paths | `synapse/nexus_config.py` |
|
||||||
| Frontend API base URL | `interface/web/src/config.js` |
|
| Frontend API base URL | `interface/web/src/config.js` |
|
||||||
| Python deps | `requirements-base.txt` + amd/nvidia GPU overlay; `requirements-wsl.txt` = standalone CPU runtime |
|
| Python deps | `requirements-base.txt` + amd/nvidia GPU overlay; `requirements-windows.txt` = standalone CPU runtime |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ cp -f "$NEXUS/management/panel/genmon-16.rc" "$PANEL_CFG/genmon-16.rc"
|
|||||||
# ── Register the genmon applets into the XFCE panel ────────────────────────
|
# ── Register the genmon applets into the XFCE panel ────────────────────────
|
||||||
# The network applet's genmon (plugin-13) was added by hand once; the Nexus
|
# The network applet's genmon (plugin-13) was added by hand once; the Nexus
|
||||||
# (15) and Bluetooth (16) applets are wired up programmatically via xfconf so a
|
# (15) and Bluetooth (16) applets are wired up programmatically via xfconf so a
|
||||||
# fresh install picks them up. No-op on machines without XFCE (e.g. the WSL box).
|
# fresh install picks them up. No-op on machines without XFCE (e.g. the Windows box).
|
||||||
# place = before|after — where to insert relative to the network applet (13).
|
# place = before|after — where to insert relative to the network applet (13).
|
||||||
register_genmon() {
|
register_genmon() {
|
||||||
local id=$1 place=$2 anchor=13 panel=panel-1
|
local id=$1 place=$2 anchor=13 panel=panel-1
|
||||||
|
|||||||
+15
-6
@@ -52,11 +52,20 @@ if [ "$stage" = "runtime" ]; then
|
|||||||
echo "Warning: bin/fetch-ollama.sh not found — skipping Ollama fetch."
|
echo "Warning: bin/fetch-ollama.sh not found — skipping Ollama fetch."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Shell wiring: the `ncp` function and the Promethean venv alias. This is the
|
# `ncp` on PATH, not a shell function: /usr/local/bin is visible to sh, cron,
|
||||||
# only place they get registered now that bin/install.sh is gone. Guarded by a
|
# systemd units and .desktop Exec lines, none of which read ~/.bashrc. The
|
||||||
# grep so an in-place restore is a no-op and a scratch clone can't re-point an
|
# Windows installer does the matching thing with management\ncp.cmd on PATH.
|
||||||
# already-wired ~/.bashrc at itself.
|
# Falls back to the old ~/.bashrc function when there's no sudo (a shell
|
||||||
if ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
|
# function shadows the symlink anyway, so having both is harmless).
|
||||||
|
ncp_link="/usr/local/bin/ncp"
|
||||||
|
ncp_target="$NEXUS_ROOT/management/nexus-cli.sh"
|
||||||
|
if [ "$(readlink -f "$ncp_link" 2>/dev/null)" = "$ncp_target" ]; then
|
||||||
|
echo "ncp already on PATH at $ncp_link."
|
||||||
|
elif sudo ln -sfn "$ncp_target" "$ncp_link"; then
|
||||||
|
echo "Registered ncp at $ncp_link."
|
||||||
|
elif ! grep -q "management/nexus-cli.sh" "$HOME/.bashrc" 2>/dev/null; then
|
||||||
|
# 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.
|
||||||
cat >> "$HOME/.bashrc" <<EOF
|
cat >> "$HOME/.bashrc" <<EOF
|
||||||
|
|
||||||
# Nexus
|
# Nexus
|
||||||
@@ -64,7 +73,7 @@ ncp() {
|
|||||||
$NEXUS_ROOT/management/nexus-cli.sh "\$@"
|
$NEXUS_ROOT/management/nexus-cli.sh "\$@"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
echo "Registered ncp in ~/.bashrc."
|
echo "No sudo for $ncp_link — registered ncp in ~/.bashrc instead."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -qF "alias promethean=" "$HOME/.bashrc" 2>/dev/null; then
|
if ! grep -qF "alias promethean=" "$HOME/.bashrc" 2>/dev/null; then
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ def venv_python() -> Path:
|
|||||||
def requirements() -> str:
|
def requirements() -> str:
|
||||||
"""Pick the PyTorch overlay for this host."""
|
"""Pick the PyTorch overlay for this host."""
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
return "requirements-wsl.txt" # CPU / pure-Python, right for native Windows
|
return "requirements-windows.txt" # CPU / pure-Python, right for native Windows
|
||||||
if shutil.which("nvidia-smi"):
|
if shutil.which("nvidia-smi"):
|
||||||
return "requirements-nvidia.txt"
|
return "requirements-nvidia.txt"
|
||||||
lspci = shutil.which("lspci")
|
lspci = shutil.which("lspci")
|
||||||
|
|||||||
+33
-47
@@ -1,7 +1,7 @@
|
|||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
NexusOS installer for Windows (native - no WSL).
|
NexusOS installer for Windows (native).
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Installs Python, Node.js, and Ollama via winget, builds the Promethean
|
Installs Python, Node.js, and Ollama via winget, builds the Promethean
|
||||||
virtualenv and the web UI, and drops a desktop shortcut. NexusOS then runs
|
virtualenv and the web UI, and drops a desktop shortcut. NexusOS then runs
|
||||||
@@ -11,17 +11,8 @@
|
|||||||
Run from the nexus-core directory:
|
Run from the nexus-core directory:
|
||||||
Right-click install-windows.ps1 -> "Run with PowerShell"
|
Right-click install-windows.ps1 -> "Run with PowerShell"
|
||||||
|
|
||||||
Requires Windows 10/11 with winget (App Installer). No WSL, no reboot.
|
Requires Windows 10/11 with winget (App Installer). No reboot needed.
|
||||||
.PARAMETER InvokerProfile
|
|
||||||
Internal. The PowerShell profile path of the user who launched the
|
|
||||||
installer, captured before self-elevation and passed to the elevated run.
|
|
||||||
If a standard account elevates with a DIFFERENT admin's credentials, the
|
|
||||||
elevated process sees that admin's $PROFILE - so ncp would be registered
|
|
||||||
for the wrong user. Not meant to be passed by hand.
|
|
||||||
#>
|
#>
|
||||||
param(
|
|
||||||
[string]$InvokerProfile = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -57,11 +48,7 @@ $IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIden
|
|||||||
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
if (-not $IsAdmin) {
|
if (-not $IsAdmin) {
|
||||||
Write-Host "Requesting administrator privileges..." -ForegroundColor Yellow
|
Write-Host "Requesting administrator privileges..." -ForegroundColor Yellow
|
||||||
# Capture THIS user's profile path before elevating and hand it to the
|
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
|
||||||
# elevated run: if UAC prompts for another admin's credentials, that process
|
|
||||||
# would otherwise register ncp in the wrong user's profile.
|
|
||||||
$Invoker = $PROFILE.CurrentUserAllHosts
|
|
||||||
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -InvokerProfile `"$Invoker`"" -Verb RunAs
|
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +87,7 @@ if (-not (Test-Path $VenvPy)) {
|
|||||||
if (-not (Test-Path $VenvPy)) { Write-Fail "venv creation failed at $Venv" }
|
if (-not (Test-Path $VenvPy)) { Write-Fail "venv creation failed at $Venv" }
|
||||||
|
|
||||||
& $VenvPy -m pip install --upgrade pip -q
|
& $VenvPy -m pip install --upgrade pip -q
|
||||||
$Req = Join-Path $RepoRoot "requirements-wsl.txt" # CPU / pure-Python overlay - right for native Windows too
|
$Req = Join-Path $RepoRoot "requirements-windows.txt" # CPU / pure-Python overlay - right for native Windows too
|
||||||
if (-not (Test-Path $Req)) { Write-Fail "requirements file not found: $Req" }
|
if (-not (Test-Path $Req)) { Write-Fail "requirements file not found: $Req" }
|
||||||
& $VenvPy -m pip install -r $Req
|
& $VenvPy -m pip install -r $Req
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Fail "pip install failed (exit $LASTEXITCODE) - see output above" }
|
if ($LASTEXITCODE -ne 0) { Write-Fail "pip install failed (exit $LASTEXITCODE) - see output above" }
|
||||||
@@ -178,41 +165,40 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# -- ncp command ---------------------------------------------------------------
|
# -- ncp command ---------------------------------------------------------------
|
||||||
# The Linux side registers ncp in ~/.bashrc from restore-linux.sh; this is the
|
# management\ncp.cmd goes on the machine PATH rather than a `function ncp` in the
|
||||||
# same idea for PowerShell. Guarded by a marker so re-running the installer is a
|
# PowerShell profile. Three reasons the profile route kept biting:
|
||||||
# no-op instead of stacking duplicate functions.
|
# 1. Execution policy defaults to Restricted, which blocks the profile itself -
|
||||||
|
# so the function was never defined, and ncp.ps1 could not have run anyway.
|
||||||
|
# 2. Profiles are a PowerShell thing. cmd, Win+R, Task Scheduler and .lnk
|
||||||
|
# targets all reported "ncp is not recognized".
|
||||||
|
# 3. We are elevated here, so $PROFILE is the ADMIN's profile - the whole
|
||||||
|
# -InvokerProfile dance existed to work around that. Machine PATH has no
|
||||||
|
# such split.
|
||||||
|
# The Linux side does the matching thing: a symlink in /usr/local/bin.
|
||||||
Write-Step "Registering the ncp command"
|
Write-Step "Registering the ncp command"
|
||||||
try {
|
try {
|
||||||
# CurrentUserAllHosts (profile.ps1), not $PROFILE: $PROFILE is host-specific,
|
$NcpCmd = Join-Path $RepoRoot "management\ncp.cmd"
|
||||||
# so ncp would exist in the console but not in the VS Code terminal or ISE.
|
if (-not (Test-Path $NcpCmd)) { throw "ncp.cmd not found at '$NcpCmd'" }
|
||||||
# Prefer the invoking user's path when we were elevated - see -InvokerProfile.
|
$BinDir = Split-Path -Parent $NcpCmd
|
||||||
$ProfilePath = if ($InvokerProfile) { $InvokerProfile } else { $PROFILE.CurrentUserAllHosts }
|
|
||||||
$ProfileDir = Split-Path -Parent $ProfilePath
|
# Machine scope needs admin, which the self-elevation above already gave us.
|
||||||
if (-not (Test-Path $ProfileDir)) {
|
# Read-modify-write through .NET, NEVER setx: setx silently truncates PATH at
|
||||||
New-Item -ItemType Directory -Path $ProfileDir -Force | Out-Null
|
# 1024 characters and has permanently broken machines.
|
||||||
}
|
$Current = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||||
$Marker = "# NexusOS ncp"
|
$Entries = @($Current -split ';' | Where-Object { $_ -ne "" })
|
||||||
$Existing = ""
|
if ($Entries -contains $BinDir) {
|
||||||
if (Test-Path $ProfilePath) {
|
Write-OK "ncp already on the machine PATH ($BinDir)"
|
||||||
$Existing = Get-Content -Path $ProfilePath -Raw -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if ($Existing -and $Existing.Contains($Marker)) {
|
|
||||||
Write-OK "ncp already registered in $ProfilePath"
|
|
||||||
} else {
|
} else {
|
||||||
$NcpPs1 = Join-Path $RepoRoot "management\ncp.ps1"
|
$New = (@($Entries) + $BinDir) -join ';'
|
||||||
# Without this, a Join-Path that fails writes `function ncp { & "" }` -
|
[Environment]::SetEnvironmentVariable("Path", $New, "Machine")
|
||||||
# a broken profile with no error. Fail into the catch below instead.
|
Write-OK "ncp registered on the machine PATH ($BinDir)"
|
||||||
if (-not (Test-Path $NcpPs1)) { throw "ncp.ps1 not found at '$NcpPs1'" }
|
|
||||||
# ASCII encoding: the content is ASCII anyway, and -Encoding UTF8 on
|
|
||||||
# PowerShell 5.1 can plant a BOM in the middle of an existing profile.
|
|
||||||
Add-Content -Path $ProfilePath -Encoding ASCII -Value ""
|
|
||||||
Add-Content -Path $ProfilePath -Encoding ASCII -Value $Marker
|
|
||||||
Add-Content -Path $ProfilePath -Encoding ASCII -Value "function ncp { & `"$NcpPs1`" @args }"
|
|
||||||
Write-OK "ncp registered in $ProfilePath"
|
|
||||||
}
|
}
|
||||||
|
# PATH is read at process start, so this shell and any already-open one still
|
||||||
|
# do not have it. Patch ours so the rest of the installer can call ncp.
|
||||||
|
$env:Path = "$env:Path;$BinDir"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Warn "Could not register ncp - add this line to your PowerShell profile:"
|
Write-Warn "Could not put ncp on PATH - add this directory to PATH by hand:"
|
||||||
Write-Warn " function ncp { & '$RepoRoot\management\ncp.ps1' @args }"
|
Write-Warn " $RepoRoot\management"
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- Desktop shortcut ----------------------------------------------------------
|
# -- Desktop shortcut ----------------------------------------------------------
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
// host: true binds 0.0.0.0 so the Windows browser can reach the dev server
|
// host: true binds 0.0.0.0 so the dev server is reachable from another
|
||||||
// through WSL2 (loopback-only binding isn't reliably forwarded out of WSL).
|
// machine on the LAN, not just localhost. Dev-only - production is the built
|
||||||
|
// dist/ served by the backend on :8000.
|
||||||
server: { host: true },
|
server: { host: true },
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# NexusOS launcher for Windows (native, no WSL, no Vite).
|
# NexusOS launcher for Windows (native, no Vite).
|
||||||
#
|
#
|
||||||
# Single-process app: the backend on :8000 serves the built web UI itself, so
|
# Single-process app: the backend on :8000 serves the built web UI itself, so
|
||||||
# this starts only the memory service + backend, then opens the UI as an Edge
|
# this starts only the memory service + backend, then opens the UI as an Edge
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class NexusControlPanel:
|
|||||||
self.root = root
|
self.root = root
|
||||||
|
|
||||||
if os.name != 'nt' and not os.environ.get('DISPLAY'):
|
if os.name != 'nt' and not os.environ.get('DISPLAY'):
|
||||||
print("ERROR: No DISPLAY variable. Check WSLg.")
|
print("ERROR: No DISPLAY variable. Is an X session running?")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
self.root.title("Nexus Control Panel")
|
self.root.title("Nexus Control Panel")
|
||||||
@@ -91,7 +91,7 @@ class NexusControlPanel:
|
|||||||
print(f"Icon load failed: {e}")
|
print(f"Icon load failed: {e}")
|
||||||
|
|
||||||
if not sys.platform.startswith("linux"):
|
if not sys.platform.startswith("linux"):
|
||||||
# Keep the custom borderless titlebar on Windows/WSL,
|
# Keep the custom borderless titlebar on Windows,
|
||||||
# but allow normal stacking on Linux Mint.
|
# but allow normal stacking on Linux Mint.
|
||||||
self.root.overrideredirect(True)
|
self.root.overrideredirect(True)
|
||||||
self.root.configure(background="#1e1e1e")
|
self.root.configure(background="#1e1e1e")
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@echo off
|
||||||
|
REM ncp - PATH entry point on Windows. A .cmd, not a .ps1, on purpose: PowerShell's
|
||||||
|
REM execution policy governs .ps1 only, so this keeps working under the default
|
||||||
|
REM Restricted policy and from cmd, Win+R, Task Scheduler and .lnk targets - none
|
||||||
|
REM of which load a PowerShell profile. install-windows.ps1 puts this directory on
|
||||||
|
REM the machine PATH. ncp.ps1 stays for pwsh-on-Linux, where there is no PATH shim.
|
||||||
|
REM
|
||||||
|
REM ASCII only, same rule as the .ps1 files - a test in tests/test_smoke.py enforces it.
|
||||||
|
setlocal
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
REM System Python fallback so backup/restore work before the venv exists; those
|
||||||
|
REM delegate to the stdlib-only bin/sync.py. Mirrors the same fallback in ncp.ps1.
|
||||||
|
set "PY=%ROOT%\Promethean\Scripts\python.exe"
|
||||||
|
if not exist "%PY%" set "PY=python"
|
||||||
|
"%PY%" "%ROOT%\management\ncp.py" %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
+4
-2
@@ -2,8 +2,10 @@
|
|||||||
# file the Linux shell wrapper runs; this only picks an interpreter and forwards
|
# file the Linux shell wrapper runs; this only picks an interpreter and forwards
|
||||||
# the arguments.
|
# the arguments.
|
||||||
#
|
#
|
||||||
# Windows: install-windows.ps1 registers this automatically. To do it by hand,
|
# On Windows, ncp comes from management/ncp.cmd on the machine PATH instead (a
|
||||||
# or to get ncp inside PowerShell on Linux, add to $PROFILE.CurrentUserAllHosts:
|
# .cmd is not subject to the execution policy and works outside PowerShell too) -
|
||||||
|
# install-windows.ps1 sets that up. This file is what gives ncp to pwsh on Linux,
|
||||||
|
# where restore-linux.sh registers it in $PROFILE.CurrentUserAllHosts as:
|
||||||
#
|
#
|
||||||
# function ncp { & "<repo>/management/ncp.ps1" @args }
|
# function ncp { & "<repo>/management/ncp.ps1" @args }
|
||||||
#
|
#
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ bin/restore-linux.sh, controlpanel.py, nexus-popup.py and nexus-app.sh all keep
|
|||||||
calling what they always called.
|
calling what they always called.
|
||||||
|
|
||||||
psutil does the process work (cmdlines, pattern sweeps, port owners). It is
|
psutil does the process work (cmdlines, pattern sweeps, port owners). It is
|
||||||
declared in requirements-base.txt AND requirements-wsl.txt, so both boxes have
|
declared in requirements-base.txt AND requirements-windows.txt, so both boxes have
|
||||||
it - but it is imported lazily so `ncp backup`/`restore` still run before the
|
it - but it is imported lazily so `ncp backup`/`restore` still run before the
|
||||||
venv exists, exactly as they did when they shelled out to sync.py.
|
venv exists, exactly as they did when they shelled out to sync.py.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Regular → Executable
+25
-5
@@ -54,19 +54,39 @@ def test_default_models_have_one_source_of_truth():
|
|||||||
assert hardcoded not in ps1, f"install-windows.ps1 hardcodes {hardcoded!r}"
|
assert hardcoded not in ps1, f"install-windows.ps1 hardcodes {hardcoded!r}"
|
||||||
|
|
||||||
|
|
||||||
def test_powershell_files_stay_ascii():
|
def test_windows_scripts_stay_ascii():
|
||||||
# PowerShell 5.1 decodes BOM-less files as ANSI: one stray Unicode dash
|
# PowerShell 5.1 decodes BOM-less files as ANSI: one stray Unicode dash
|
||||||
# eats a quote and the whole script dies at parse time. Globbed rather than
|
# eats a quote and the whole script dies at parse time. cmd.exe is worse
|
||||||
# listed by name so a newly added .ps1 is covered without editing this test.
|
# still - it decodes by the console codepage. Globbed rather than listed by
|
||||||
|
# name so a newly added script is covered without editing this test.
|
||||||
skip = {"Promethean", "node_modules", ".git", "dist"}
|
skip = {"Promethean", "node_modules", ".git", "dist"}
|
||||||
ps1s = [p for p in REPO_ROOT.rglob("*.ps1") if not skip & set(p.parts)]
|
ps1s = [p for pat in ("*.ps1", "*.cmd") for p in REPO_ROOT.rglob(pat)
|
||||||
assert ps1s, "no .ps1 files found - did the Windows path move?"
|
if not skip & set(p.parts)]
|
||||||
|
assert ps1s, "no .ps1/.cmd files found - did the Windows path move?"
|
||||||
for path in ps1s:
|
for path in ps1s:
|
||||||
raw = path.read_bytes()
|
raw = path.read_bytes()
|
||||||
bad = [(i, b) for i, b in enumerate(raw) if b > 0x7F]
|
bad = [(i, b) for i, b in enumerate(raw) if b > 0x7F]
|
||||||
assert not bad, f"{path.relative_to(REPO_ROOT)} has non-ASCII bytes at {bad[:3]}"
|
assert not bad, f"{path.relative_to(REPO_ROOT)} has non-ASCII bytes at {bad[:3]}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ncp_is_registered_on_path_not_in_a_shell_profile():
|
||||||
|
"""A `function ncp` in a shell profile is invisible to cron, .desktop Exec
|
||||||
|
lines, cmd.exe and Task Scheduler - and on Windows the default Restricted
|
||||||
|
execution policy blocks the profile outright. Both installers must put ncp
|
||||||
|
on PATH; the profile wiring only survives as a no-sudo fallback."""
|
||||||
|
linux = (REPO_ROOT / "bin" / "restore-linux.sh").read_text(encoding="utf-8")
|
||||||
|
runtime = linux.split('if [ "$stage" = "runtime" ]; then')[1].split("\n exit 0\nfi")[0]
|
||||||
|
assert "/usr/local/bin/ncp" in runtime
|
||||||
|
|
||||||
|
ps1 = (REPO_ROOT / "install-windows.ps1").read_text(encoding="utf-8")
|
||||||
|
assert "ncp.cmd" in ps1, "installer must register the .cmd shim, not a profile function"
|
||||||
|
# setx truncates PATH at 1024 characters and has permanently broken machines.
|
||||||
|
# Comments stripped so the code that explains the ban does not trip it.
|
||||||
|
code = "\n".join(ln for ln in ps1.splitlines() if not ln.strip().startswith("#"))
|
||||||
|
assert "setx" not in code.lower()
|
||||||
|
assert 'SetEnvironmentVariable("Path"' in code
|
||||||
|
|
||||||
|
|
||||||
def test_first_playbook_is_the_system_prompt(tmp_path, monkeypatch):
|
def test_first_playbook_is_the_system_prompt(tmp_path, monkeypatch):
|
||||||
store = PlaybookFileStore(tmp_path)
|
store = PlaybookFileStore(tmp_path)
|
||||||
store.add_playbook(PlaybookItem(id="ctx", title="Reference", goal="ref goal",
|
store.add_playbook(PlaybookItem(id="ctx", title="Reference", goal="ref goal",
|
||||||
|
|||||||
Reference in New Issue
Block a user