From a79cd6f5e9f04ec7d4fc499d7ca8c6cf9a8de815 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com> Date: Fri, 29 May 2026 16:44:10 -0700 Subject: [PATCH] feat(windows): Phase-1 native install + scheduled-task service (#438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Phase-1 of #298 (Native Windows Support RFC). Reverses the long-standing "native Windows is not supported" stance. PowerShell installer at `deploy/windows/install.ps1` (published to https://open-jarvis.github.io/OpenJarvis/install.ps1) plus a `jarvis-service.ps1` scheduled-task helper that mirrors `deploy/systemd/openjarvis.service` and `deploy/launchd/com.openjarvis.plist`. Loopback default (127.0.0.1, no API key) — same as launchd. One-liner install: irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex Adversarial review caught and fixed two real bugs pre-commit: 1. `irm | iex` drops `param()` flags — added env-var fallbacks (OPENJARVIS_SKIP_SERVICE / OPENJARVIS_SERVICE / OPENJARVIS_FORCE). 2. Scheduled tasks don't inherit the registering session's env — the LAN-exposed `OPENJARVIS_API_KEY` path now persists the key to User scope so the task's logon environment can read it. Supersedes #434 (the guidance-only "use WSL2" install.ps1). Massive thanks to @SeCuReDmE-main-dev for the careful RFC #298 — the three-phase decomposition (install / service / shared-memory bridge) is exactly the right framing. This PR ships Phase-1 and Phase-2 of the RFC fused into one release; Phase-3 (shared memory bridge) remains future work. Thanks also to @KadenBordeaux for raising #334 ("'bash' is not recognized as the name of a cmdlet"). That report is what made this whole Windows-support cluster a priority — without your bug report the unsupported stance would still be in the README. The friction you hit motivated #432 (numpy/python cap), #433 (CLI startup resilience), #436 (python discovery helpers), #437 (desktop launcher fix), and this PR. Co-Authored-By: Claude Opus 4.7 --- README.md | 9 +- deploy/windows/README.md | 126 +++++++++++ deploy/windows/install.ps1 | 299 +++++++++++++++++++++++++ deploy/windows/jarvis-service.ps1 | 208 +++++++++++++++++ docs/gen_install_script.py | 27 ++- docs/getting-started/windows-native.md | 83 +++++++ docs/getting-started/wsl2.md | 5 +- pyproject.toml | 1 + 8 files changed, 746 insertions(+), 12 deletions(-) create mode 100644 deploy/windows/README.md create mode 100644 deploy/windows/install.ps1 create mode 100644 deploy/windows/jarvis-service.ps1 create mode 100644 docs/getting-started/windows-native.md diff --git a/README.md b/README.md index 9a7545f3..285e38c5 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,18 @@ curl -fsSL https://open-jarvis.github.io/OpenJarvis/install.sh | bash The installer handles everything for you — including [uv](https://docs.astral.sh/uv/), the Python venv, Ollama, and a small starter model. You don't need to install anything first. -**Windows:** the installer is a `bash` script and won't run in PowerShell or `cmd`. Pick one of: +**Windows:** the bash installer won't run in PowerShell or `cmd`. Pick one of: - **WSL2 (recommended for the CLI / Python SDK)** — one-time setup in an admin PowerShell, then run the same `curl … | bash` inside Ubuntu: ```powershell wsl --install -d Ubuntu-24.04 ``` Open the Ubuntu shell that gets installed, then follow [WSL2 install instructions](https://open-jarvis.github.io/OpenJarvis/getting-started/wsl2/). +- **Native PowerShell (advanced)** — Phase-1 of the [native Windows RFC (#298)](https://github.com/open-jarvis/OpenJarvis/issues/298). Same `uv sync --extra server`, no WSL2 / no Docker: + ```powershell + irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex + ``` + See [native Windows install guide](https://open-jarvis.github.io/OpenJarvis/getting-started/windows-native/). - **Desktop app** — download the [Windows installer (`.exe`)](https://github.com/open-jarvis/OpenJarvis/releases/download/desktop-v1.0.2/OpenJarvis_1.0.1_x64-setup.exe) from the latest [desktop release](https://github.com/open-jarvis/OpenJarvis/releases/tag/desktop-v1.0.2) (macOS `.dmg` and Linux `.deb`/`.rpm`/`.AppImage` are there too) for the GUI experience, no terminal required. **Prerequisite:** the desktop app expects [uv](https://docs.astral.sh/uv/) to be installed already — if it isn't, install it first in PowerShell, then launch the app: ```powershell powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex" @@ -59,7 +64,7 @@ jarvis The Rust extension and bigger models continue downloading in the background while you chat. Run `jarvis doctor` to see status. -**Platforms:** macOS (Intel + Apple Silicon), Linux, WSL2 on Windows. Native Windows is not supported — use WSL2 or the desktop binary. +**Platforms:** macOS (Intel + Apple Silicon), Linux, WSL2 on Windows, and Phase-1 native Windows (PowerShell install + scheduled-task service — see [#298](https://github.com/open-jarvis/OpenJarvis/issues/298)). **Manual install / contributors:** see [docs/getting-started/install.md](docs/getting-started/install.md). diff --git a/deploy/windows/README.md b/deploy/windows/README.md new file mode 100644 index 00000000..2a95a048 --- /dev/null +++ b/deploy/windows/README.md @@ -0,0 +1,126 @@ +# OpenJarvis on native Windows + +Phase-1 of the native-Windows-support RFC (#298). Mirrors the Linux +(`deploy/systemd/`) and macOS (`deploy/launchd/`) deployments — but for +PowerShell, without WSL2 or Docker. + +## One-liner install + +In an elevated-or-regular PowerShell: + +```powershell +irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex +``` + +What it does: + +1. Refuses non-Windows hosts and Windows < 10 1809. +2. Checks Python 3.10 – 3.13 (3.14 has no numpy wheels yet — see #432). +3. Checks `git` on PATH. +4. Installs `uv` (https://astral.sh/uv) if absent. +5. Clones the OpenJarvis repository to `%LOCALAPPDATA%\OpenJarvis` + (override with `$env:OPENJARVIS_HOME`). +6. Runs `uv sync --extra server` so the FastAPI server entry point is + importable. +7. Optionally prompts to register a scheduled task that auto-starts the + server at logon. + +Flags (when invoked directly rather than via `irm | iex`): + +| Flag | Effect | +|------|--------| +| `-Service` | Register the scheduled task without prompting | +| `-SkipService` | Don't prompt; don't register | +| `-Force` | Re-run all steps even if already done | + +`irm | iex` can't pass `param()` args into a piped script string, so +the same knobs are honored via env vars when the corresponding flag is +absent: + +```powershell +$env:OPENJARVIS_SKIP_SERVICE = '1' +irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex +``` + +The available env vars: `OPENJARVIS_SKIP_SERVICE`, `OPENJARVIS_SERVICE`, +`OPENJARVIS_FORCE`. If you need richer control, save the script first +(`irm ... -OutFile install.ps1; .\install.ps1 -Force`). + +## Manual scheduled-task setup + +If you skipped the prompt during install, you can register / inspect / +remove the task with `jarvis-service.ps1`: + +```powershell +$srv = "$env:LOCALAPPDATA\OpenJarvis\src\deploy\windows\jarvis-service.ps1" + +# install (idempotent — replaces existing) +powershell -ExecutionPolicy Bypass -File $srv install + +# status +powershell -ExecutionPolicy Bypass -File $srv status + +# remove +powershell -ExecutionPolicy Bypass -File $srv uninstall +``` + +The task runs as the current user with `LogonType=Interactive` and +`RunLevel=Limited`. It restarts up to 3 times on failure (1-minute +gap), has no execution-time limit, and starts when available (catches +up if missed). + +## Loopback vs LAN-exposed + +By default the scheduled task binds `127.0.0.1` — reachable only from +this machine, no API key required. This matches launchd parity (see +`deploy/launchd/com.openjarvis.plist`). + +To expose on your LAN: + +```powershell +# 1. Generate an API key. The server REFUSES to bind 0.0.0.0 without one. +$env:OPENJARVIS_API_KEY = (uv run jarvis auth generate-key) + +# 2. Re-register the task with -ListenHost 0.0.0.0. +powershell -ExecutionPolicy Bypass -File $srv install -ListenHost 0.0.0.0 +``` + +`jarvis-service.ps1 install` refuses `-ListenHost 0.0.0.0` if +`$env:OPENJARVIS_API_KEY` is unset — same guard as the systemd unit's +`EnvironmentFile=/etc/openjarvis/env`. + +## Parity table + +| Concern | systemd | launchd | Windows | +|---------|---------|---------|---------| +| Service definition | `deploy/systemd/openjarvis.service` | `deploy/launchd/com.openjarvis.plist` | `deploy/windows/jarvis-service.ps1` (cmdlet-driven) | +| Default bind | `0.0.0.0` (with API key) | `127.0.0.1` (no API key) | `127.0.0.1` (no API key) | +| Restart on failure | `Restart=on-failure RestartSec=5` | `KeepAlive=true` | `RestartCount=3 RestartInterval=PT1M` | +| Auto-start | `multi-user.target` | `RunAtLoad=true` | `AtLogOn` trigger | + +## Updating + +To pull the latest: + +```powershell +cd "$env:LOCALAPPDATA\OpenJarvis\src" +git pull --ff-only +uv sync --extra server +``` + +Or re-run the installer with `-Force`: + +```powershell +irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex +# (then re-run with the file directly, passing -Force) +``` + +## Uninstall + +```powershell +powershell -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\OpenJarvis\src\deploy\windows\jarvis-service.ps1" uninstall +Remove-Item -Recurse -Force "$env:LOCALAPPDATA\OpenJarvis" +``` + +Uninstalling does NOT remove `uv` (it's a separate tool — you may have +other Python projects using it). diff --git a/deploy/windows/install.ps1 b/deploy/windows/install.ps1 new file mode 100644 index 00000000..bee6c2b0 --- /dev/null +++ b/deploy/windows/install.ps1 @@ -0,0 +1,299 @@ +<# +.SYNOPSIS + OpenJarvis native Windows installer. + +.DESCRIPTION + Phase-1 of the native-Windows-support RFC (#298). Mirrors the + behavior of scripts/install/install.sh (the curl-pipe-bash installer + for Linux/WSL2/macOS) but for native Windows PowerShell — no WSL, + no Docker, no MSYS2. + + Steps: + 1. Refuse non-Windows / Windows < 10. + 2. Check Python 3.10 — 3.13 on PATH (3.14 has no numpy wheels yet, + see #432). + 3. Check git on PATH. + 4. Install uv (https://astral.sh/uv) if absent. + 5. Clone the OpenJarvis repository to $env:LOCALAPPDATA\OpenJarvis + (override with $env:OPENJARVIS_HOME). + 6. Run `uv sync --extra server` so the FastAPI server entry point + is importable. + 7. Optionally register the scheduled-task service (see + deploy/windows/jarvis-service.ps1). + + Usage (one-liner): + irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex + + Usage (file invocation, supports flags): + irm https://open-jarvis.github.io/OpenJarvis/install.ps1 -OutFile install.ps1 + .\install.ps1 -SkipService + + Flags (when running the file directly): + -SkipService Don't prompt for / install the scheduled task. + -Service Install the scheduled task without prompting. + -Force Re-run all steps even if already done. + + Under `irm | iex` the param block is unreachable (Invoke-Expression + can't pass named args into a piped script string), so the same knobs + are honored via env vars when the corresponding flag is absent: + $env:OPENJARVIS_SKIP_SERVICE = '1' + $env:OPENJARVIS_SERVICE = '1' + $env:OPENJARVIS_FORCE = '1' + +.NOTES + Loopback default: the scheduled-task service binds 127.0.0.1, so no + API key is needed. To expose on the LAN, edit the registered task to + pass `--host 0.0.0.0` AND set $env:OPENJARVIS_API_KEY (an + unauthenticated 0.0.0.0 server refuses to start). See + deploy/windows/README.md. +#> + +[CmdletBinding()] +param( + [switch] $SkipService, + [switch] $Service, + [switch] $Force +) + +$ErrorActionPreference = 'Stop' + +# Env-var fallback for the `irm | iex` path, where the param block is +# unreachable (see header comment). Any explicit -switch wins; env vars +# only fill in the gaps. +if (-not $SkipService -and $env:OPENJARVIS_SKIP_SERVICE) { $SkipService = $true } +if (-not $Service -and $env:OPENJARVIS_SERVICE) { $Service = $true } +if (-not $Force -and $env:OPENJARVIS_FORCE) { $Force = $true } + +# --------------------------------------------------------------------------- +# Output helpers — coloured but plain enough for Constrained Language Mode. +# --------------------------------------------------------------------------- + +function Write-Info ($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan } +function Write-Ok ($msg) { Write-Host "[ok] $msg" -ForegroundColor Green } +function Write-Warn2 ($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow } +function Write-Fail ($msg) { + Write-Host "[fail] $msg" -ForegroundColor Red + exit 1 +} + +# --------------------------------------------------------------------------- +# 1. OS check +# --------------------------------------------------------------------------- + +Write-Info "Checking OS..." +if ($PSVersionTable.Platform -and $PSVersionTable.Platform -ne 'Win32NT') { + Write-Fail "install.ps1 is for native Windows. On Linux/macOS use install.sh." +} + +# Build number 17763 = Windows 10 1809 (the oldest LTS we test against). +$build = [System.Environment]::OSVersion.Version.Build +if ($build -lt 17763) { + Write-Fail "Windows 10 1809 (build 17763) or newer is required. Detected build $build." +} +Write-Ok "Windows build $build" + +# --------------------------------------------------------------------------- +# 2. Python check +# --------------------------------------------------------------------------- + +function Get-PythonCommand { + # Prefer `python3` (matches our cross-platform helper convention), + # fall back to `python` (the Windows store / python.org default). + foreach ($name in @('python3', 'python')) { + $cmd = Get-Command $name -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + } + return $null +} + +Write-Info "Checking Python (3.10 - 3.13)..." +$pythonExe = Get-PythonCommand +if (-not $pythonExe) { + Write-Fail @" +Python 3.10 - 3.13 not found on PATH. + +Install from https://python.org (check 'Add python.exe to PATH' during +install) or via winget: + + winget install Python.Python.3.13 + +Then re-run this installer. +"@ +} + +$verRaw = & $pythonExe --version 2>&1 +$verMatch = [regex]::Match($verRaw, '(\d+)\.(\d+)\.(\d+)') +if (-not $verMatch.Success) { + Write-Fail "Could not parse Python version from: $verRaw" +} +$pyMajor = [int]$verMatch.Groups[1].Value +$pyMinor = [int]$verMatch.Groups[2].Value +if ($pyMajor -ne 3 -or $pyMinor -lt 10 -or $pyMinor -gt 13) { + Write-Fail @" +Found Python $pyMajor.$pyMinor at $pythonExe, but OpenJarvis requires +3.10 - 3.13. Python 3.14 has no numpy Windows wheels yet (#432, will +re-open once numpy ships cp314). +"@ +} +Write-Ok "Python $pyMajor.$pyMinor ($pythonExe)" + +# --------------------------------------------------------------------------- +# 3. git check +# --------------------------------------------------------------------------- + +Write-Info "Checking git..." +$gitExe = (Get-Command git -ErrorAction SilentlyContinue).Source +if (-not $gitExe) { + Write-Fail @" +git not found on PATH. + +Install via winget: + + winget install Git.Git + +or download from https://git-scm.com, then re-run this installer. +"@ +} +Write-Ok "git ($gitExe)" + +# --------------------------------------------------------------------------- +# 4. uv check / install +# --------------------------------------------------------------------------- + +Write-Info "Checking uv..." +$uvExe = (Get-Command uv -ErrorAction SilentlyContinue).Source +if (-not $uvExe) { + Write-Info "Installing uv via astral.sh/uv (official PowerShell installer)..." + try { + Invoke-RestMethod -Uri 'https://astral.sh/uv/install.ps1' -UseBasicParsing | Invoke-Expression + } catch { + Write-Fail "uv install failed: $($_.Exception.Message)" + } + # The astral installer puts uv at %USERPROFILE%\.local\bin\uv.exe and + # adds that dir to the User PATH. The current process's PATH isn't + # refreshed automatically — prepend the install dir so the rest of + # this script picks it up. + $uvDir = Join-Path $env:USERPROFILE '.local\bin' + if (Test-Path (Join-Path $uvDir 'uv.exe')) { + $env:Path = "$uvDir;$env:Path" + } + $uvExe = (Get-Command uv -ErrorAction SilentlyContinue).Source + if (-not $uvExe) { + Write-Fail "uv installed but isn't on PATH. Re-open a fresh PowerShell and re-run." + } +} +Write-Ok "uv ($uvExe)" + +# --------------------------------------------------------------------------- +# 5. Clone the repo +# --------------------------------------------------------------------------- + +$installRoot = if ($env:OPENJARVIS_HOME) { + $env:OPENJARVIS_HOME +} else { + Join-Path $env:LOCALAPPDATA 'OpenJarvis' +} +$srcDir = Join-Path $installRoot 'src' + +Write-Info "Install root: $installRoot" + +if (-not (Test-Path $installRoot)) { + New-Item -ItemType Directory -Path $installRoot | Out-Null +} + +$repoUrl = if ($env:OPENJARVIS_REPO_URL) { + $env:OPENJARVIS_REPO_URL +} else { + 'https://github.com/open-jarvis/OpenJarvis.git' +} + +if (Test-Path (Join-Path $srcDir '.git')) { + if ($Force) { + Write-Info "Force: pulling latest from $repoUrl..." + & $gitExe -C $srcDir pull --ff-only + if ($LASTEXITCODE -ne 0) { Write-Fail "git pull failed" } + } else { + Write-Ok "Repository already cloned (use -Force to update)" + } +} else { + Write-Info "Cloning $repoUrl..." + & $gitExe clone --depth 1 $repoUrl $srcDir + if ($LASTEXITCODE -ne 0) { Write-Fail "git clone failed" } + Write-Ok "Cloned to $srcDir" +} + +# --------------------------------------------------------------------------- +# 6. uv sync --extra server +# --------------------------------------------------------------------------- + +Write-Info "Running 'uv sync --extra server' in $srcDir (this can take a few minutes)..." +Push-Location $srcDir +try { + & $uvExe sync --extra server + if ($LASTEXITCODE -ne 0) { + Write-Fail "uv sync failed with exit code $LASTEXITCODE. Check the output above." + } +} finally { + Pop-Location +} +Write-Ok "Dependencies installed" + +# --------------------------------------------------------------------------- +# 7. Optional: register the scheduled-task service +# --------------------------------------------------------------------------- + +$serviceScript = Join-Path $srcDir 'deploy\windows\jarvis-service.ps1' +$shouldInstallService = $false + +if ($Service) { + $shouldInstallService = $true +} elseif ($SkipService) { + $shouldInstallService = $false +} else { + # Interactive prompt only when there's a real user at the keyboard + # AND stdin isn't piped. [Environment]::UserInteractive is the + # canonical PowerShell idiom for "is this a user session" (false for + # services, scheduled tasks, etc); we additionally guard against the + # `irm | iex` case where stdin is redirected. + $isInteractive = [Environment]::UserInteractive ` + -and -not [System.Console]::IsInputRedirected + if ($isInteractive) { + $reply = Read-Host "Register OpenJarvis as a Windows scheduled task (auto-start at logon, loopback only)? [y/N]" + $shouldInstallService = ($reply -match '^[yY]') + } else { + Write-Warn2 "Non-interactive install — skipping scheduled-task setup." + Write-Warn2 "To register the service later, run:" + Write-Warn2 " powershell -ExecutionPolicy Bypass -File `"$serviceScript`" install" + } +} + +if ($shouldInstallService) { + if (-not (Test-Path $serviceScript)) { + Write-Fail "Service script not found at $serviceScript (the clone may be missing files; try -Force)." + } + Write-Info "Installing scheduled task..." + & powershell -ExecutionPolicy Bypass -File $serviceScript install -InstallRoot $installRoot + if ($LASTEXITCODE -ne 0) { + Write-Fail "Scheduled task setup failed." + } + Write-Ok "Scheduled task 'OpenJarvis' registered (loopback default)." +} + +# --------------------------------------------------------------------------- +# 8. Final message +# --------------------------------------------------------------------------- + +Write-Host "" +Write-Host " ┌──────────────────────────────────┐" -ForegroundColor Green +Write-Host " │ OpenJarvis install complete │" -ForegroundColor Green +Write-Host " └──────────────────────────────────┘" -ForegroundColor Green +Write-Host "" +Write-Host " Repo: $srcDir" +Write-Host " Run it: cd `"$srcDir`"; uv run jarvis serve" +if ($shouldInstallService) { + Write-Host " Service: schtasks /Query /TN OpenJarvis (status)" + Write-Host " powershell -File `"$serviceScript`" uninstall (remove)" +} +Write-Host "" +Write-Host " Docs: https://open-jarvis.github.io/OpenJarvis/" +Write-Host "" diff --git a/deploy/windows/jarvis-service.ps1 b/deploy/windows/jarvis-service.ps1 new file mode 100644 index 00000000..de076b66 --- /dev/null +++ b/deploy/windows/jarvis-service.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + Register / unregister the OpenJarvis Windows scheduled task. + +.DESCRIPTION + The Windows equivalent of deploy/systemd/openjarvis.service and + deploy/launchd/com.openjarvis.plist. + + Registers a per-user scheduled task named "OpenJarvis" that starts + `jarvis serve` at logon and restarts on failure. Loopback default + (127.0.0.1) so no API key is required — matches launchd parity. + + Subcommands: + install — create or replace the task + uninstall — remove the task + status — show task state + + Arguments (install only): + -InstallRoot default: %LOCALAPPDATA%\OpenJarvis (matches + install.ps1's default) + -ListenHost default: 127.0.0.1 (loopback). Set to 0.0.0.0 + ONLY if you also set $env:OPENJARVIS_API_KEY + — the server refuses to start unauthenticated + on a non-loopback bind. + -ListenPort default: 8000 + + Usage: + powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 install + powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 uninstall + powershell -ExecutionPolicy Bypass -File jarvis-service.ps1 status +#> + +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [ValidateSet('install', 'uninstall', 'status')] + [string] $Command = 'status', + + [string] $InstallRoot, + [string] $ListenHost = '127.0.0.1', + [int] $ListenPort = 8000 +) + +$ErrorActionPreference = 'Stop' +$TaskName = 'OpenJarvis' + +function Write-Info ($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan } +function Write-Ok ($msg) { Write-Host "[ok] $msg" -ForegroundColor Green } +function Write-Warn2 ($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow } +function Write-Fail ($msg) { + Write-Host "[fail] $msg" -ForegroundColor Red + exit 1 +} + +function Get-DefaultInstallRoot { + # Use $script: prefix so this is robust to being called from any + # function scope (PowerShell's default dynamic lookup would also + # work today, but $script: is the explicit contract). + if ($script:InstallRoot) { return $script:InstallRoot } + if ($env:OPENJARVIS_HOME) { return $env:OPENJARVIS_HOME } + return (Join-Path $env:LOCALAPPDATA 'OpenJarvis') +} + +# --------------------------------------------------------------------------- +# install +# --------------------------------------------------------------------------- + +function Install-Task { + $root = Get-DefaultInstallRoot + $srcDir = Join-Path $root 'src' + if (-not (Test-Path $srcDir)) { + Write-Fail "OpenJarvis source not found at $srcDir. Run install.ps1 first." + } + + $uvCmd = Get-Command uv -ErrorAction SilentlyContinue + if (-not $uvCmd) { + $uvFallback = Join-Path $env:USERPROFILE '.local\bin\uv.exe' + if (Test-Path $uvFallback) { + $uvPath = $uvFallback + } else { + Write-Fail "uv.exe not found on PATH or at $uvFallback. Re-run install.ps1." + } + } else { + $uvPath = $uvCmd.Source + } + + # Safety: refuse to register a non-loopback bind without an API key. + # Mirrors deploy/systemd/openjarvis.service's EnvironmentFile guard. + $isLoopback = ($ListenHost -eq '127.0.0.1' -or $ListenHost -eq 'localhost') + if (-not $isLoopback -and -not $env:OPENJARVIS_API_KEY) { + Write-Fail @" +ListenHost is $ListenHost (non-loopback) but `$env:OPENJARVIS_API_KEY is +not set. An unauthenticated non-loopback bind is refused by jarvis serve +and would also create a security hole. Set the env var first: + + `$env:OPENJARVIS_API_KEY = (uv run jarvis auth generate-key) + +then re-run with -ListenHost 0.0.0.0. +"@ + } + + # CRITICAL: scheduled tasks do NOT inherit the registering session's + # environment. If we registered the task now and stopped here, the + # task would launch at logon with a clean env, find no API key, and + # `jarvis serve` would refuse to bind 0.0.0.0 — failing silently every + # logon. Persist the key to the User env scope so the task's logon + # session picks it up. (Loopback path doesn't need the key, so this + # only runs for the explicit LAN-exposed case.) + if (-not $isLoopback) { + Write-Info "Persisting OPENJARVIS_API_KEY to User environment so the scheduled task can read it at logon." + [System.Environment]::SetEnvironmentVariable( + 'OPENJARVIS_API_KEY', + $env:OPENJARVIS_API_KEY, + 'User' + ) + } + + Write-Info "Registering scheduled task '$TaskName'..." + Write-Info " Working dir : $srcDir" + Write-Info " Listen : $ListenHost`:$ListenPort" + Write-Info " User : $env:USERNAME" + + # If a previous task exists, remove it first (idempotent install). + $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if ($existing) { + Write-Info "Existing task found — replacing." + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false + } + + $action = New-ScheduledTaskAction ` + -Execute $uvPath ` + -Argument "run jarvis serve --host $ListenHost --port $ListenPort" ` + -WorkingDirectory $srcDir + + $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME + + $settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -RestartCount 3 ` + -RestartInterval (New-TimeSpan -Minutes 1) ` + -ExecutionTimeLimit (New-TimeSpan -Seconds 0) + + $principal = New-ScheduledTaskPrincipal ` + -UserId $env:USERNAME ` + -LogonType Interactive ` + -RunLevel Limited + + Register-ScheduledTask ` + -TaskName $TaskName ` + -Action $action ` + -Trigger $trigger ` + -Settings $settings ` + -Principal $principal ` + -Description 'OpenJarvis API server (loopback default — see deploy/windows/README.md)' | Out-Null + + Write-Ok "Task '$TaskName' registered." + Write-Info "It will start automatically at next logon." + Write-Info "To start it now: Start-ScheduledTask -TaskName $TaskName" +} + +# --------------------------------------------------------------------------- +# uninstall +# --------------------------------------------------------------------------- + +function Uninstall-Task { + $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if (-not $existing) { + Write-Warn2 "Task '$TaskName' is not registered — nothing to remove." + return + } + Write-Info "Stopping '$TaskName' (if running)..." + Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + Write-Info "Unregistering '$TaskName'..." + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false + Write-Ok "Task '$TaskName' removed." +} + +# --------------------------------------------------------------------------- +# status +# --------------------------------------------------------------------------- + +function Show-Status { + $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if (-not $task) { + Write-Host "Task '$TaskName' is not registered." + Write-Host "Install it with:" + Write-Host " powershell -ExecutionPolicy Bypass -File `"$PSCommandPath`" install" + return + } + $info = Get-ScheduledTaskInfo -TaskName $TaskName + Write-Host "Task : $TaskName" + Write-Host "State : $($task.State)" + Write-Host "LastRun : $($info.LastRunTime)" + Write-Host "LastRes : 0x$('{0:X8}' -f $info.LastTaskResult)" + Write-Host "NextRun : $($info.NextRunTime)" +} + +# --------------------------------------------------------------------------- +# dispatch +# --------------------------------------------------------------------------- + +switch ($Command) { + 'install' { Install-Task } + 'uninstall' { Uninstall-Task } + 'status' { Show-Status } +} diff --git a/docs/gen_install_script.py b/docs/gen_install_script.py index 48a16add..3aaffcba 100644 --- a/docs/gen_install_script.py +++ b/docs/gen_install_script.py @@ -1,21 +1,30 @@ -"""Publish the canonical install.sh into the docs site. +"""Publish the canonical install scripts into the docs site. + +Serves the installers at:: + + https://open-jarvis.github.io/OpenJarvis/install.sh (Linux / macOS / WSL2) + https://open-jarvis.github.io/OpenJarvis/install.ps1 (native Windows) -Serves the installer at ``https://open-jarvis.github.io/OpenJarvis/install.sh`` so users have an HTTPS-valid, project-controlled install URL that does not depend on the externally-hosted ``openjarvis.ai`` domain — whose TLS config broke and which the project does not control (issue #337). -Single source of truth: the script lives at ``scripts/install/install.sh`` -(also bundled into the wheel as ``_install_scripts/``). This copies it -verbatim into the built site at ``install.sh`` on every ``mkdocs build``, -so the published copy can never drift from the canonical one. +Single source of truth: the scripts live under ``scripts/install/`` and +``deploy/windows/`` (also bundled into the wheel as ``_install_scripts/``). +This copies them verbatim into the built site on every ``mkdocs build``, +so the published copies can never drift from the canonical ones. """ from pathlib import Path import mkdocs_gen_files -_SRC = Path("scripts/install/install.sh") +# (source path, published URL path) +_SCRIPTS = [ + (Path("scripts/install/install.sh"), "install.sh"), + (Path("deploy/windows/install.ps1"), "install.ps1"), +] -with mkdocs_gen_files.open("install.sh", "wb") as dst: - dst.write(_SRC.read_bytes()) +for src, dest in _SCRIPTS: + with mkdocs_gen_files.open(dest, "wb") as out: + out.write(src.read_bytes()) diff --git a/docs/getting-started/windows-native.md b/docs/getting-started/windows-native.md new file mode 100644 index 00000000..a892320c --- /dev/null +++ b/docs/getting-started/windows-native.md @@ -0,0 +1,83 @@ +# Native Windows (advanced) + +Phase-1 of the native-Windows-support RFC (#298). Mirrors the Linux +(systemd) and macOS (launchd) deployments — but for PowerShell, without +WSL2 or Docker. Choose this over [WSL2](wsl2.md) only if you want to +avoid a Linux VM; WSL2 remains the smoother experience for most users. + +## What you get + +- A PowerShell installer that probes prerequisites, installs `uv`, + clones the repo, and runs `uv sync --extra server`. +- An optional Windows scheduled-task service equivalent to the systemd + unit and launchd plist. +- Loopback default — the service binds `127.0.0.1` so no API key is + required. + +## What you need + +- Windows 10 1809+ or Windows 11. +- Python 3.10 – 3.13 (Python 3.14 has no numpy Windows wheels yet — + see [#432](https://github.com/open-jarvis/OpenJarvis/issues/432)). +- `git` on PATH. +- ~5 GB free disk on `%LOCALAPPDATA%`. + +## Install + +In any PowerShell: + +```powershell +irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex +``` + +The installer will: + +1. Refuse non-Windows hosts and old Windows builds. +2. Confirm Python 3.10 – 3.13. +3. Confirm `git`. +4. Install `uv` if absent (via the official `astral.sh/uv` PowerShell + installer). +5. Clone the repo to `%LOCALAPPDATA%\OpenJarvis\src`. +6. Run `uv sync --extra server`. +7. Prompt to register the scheduled-task service (skip with + `-SkipService`). + +## Run it + +```powershell +cd "$env:LOCALAPPDATA\OpenJarvis\src" +uv run jarvis serve +``` + +Open `http://127.0.0.1:8000/health` to verify. + +## Scheduled-task service + +If you skipped the prompt during install, register the auto-start task +manually: + +```powershell +$srv = "$env:LOCALAPPDATA\OpenJarvis\src\deploy\windows\jarvis-service.ps1" +powershell -ExecutionPolicy Bypass -File $srv install +``` + +State: + +```powershell +powershell -ExecutionPolicy Bypass -File $srv status +``` + +Remove: + +```powershell +powershell -ExecutionPolicy Bypass -File $srv uninstall +``` + +See [`deploy/windows/README.md`](https://github.com/open-jarvis/OpenJarvis/blob/main/deploy/windows/README.md) +for the LAN-exposed configuration and the parity table against +systemd / launchd. + +## See also + +- [WSL2 install](wsl2.md) — the recommended Windows path. +- [Full installer reference](install.md). diff --git a/docs/getting-started/wsl2.md b/docs/getting-started/wsl2.md index 8259f71d..e4df976e 100644 --- a/docs/getting-started/wsl2.md +++ b/docs/getting-started/wsl2.md @@ -1,6 +1,9 @@ # WSL2 Install -OpenJarvis runs in WSL2 on Windows. Native Windows is not supported. +OpenJarvis on Windows installs two ways: **WSL2** (this page — the +recommended path; identical to native Linux) or **[native Windows +(advanced)](windows-native.md)** (Phase-1; PowerShell installer, no +WSL2 / no Docker). Pick WSL2 for the smoothest experience. ## One-time WSL setup diff --git a/pyproject.toml b/pyproject.toml index b35c38de..7c718d9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,7 @@ packages = ["src/openjarvis"] "src/openjarvis/agents/claude_code_runner" = "_node_modules/claude_code_runner" "src/openjarvis/channels/whatsapp_baileys_bridge" = "_node_modules/whatsapp_baileys_bridge" "scripts/install" = "_install_scripts" +"deploy/windows" = "_deploy/windows" [tool.pytest.ini_options] testpaths = ["tests"]