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:
jon
2026-07-22 14:11:18 -05:00
co-authored by Claude Opus 4.8
parent 9befa1d561
commit 6137710c25
14 changed files with 124 additions and 91 deletions
+3 -3
View File
@@ -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` |
+19 -20
View File
@@ -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 |
--- ---
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 ----------------------------------------------------------
+3 -2
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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")
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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.
""" """
View File
+25 -5
View File
@@ -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",