Files
OpenJarvis/deploy/windows/install.ps1
T
a79cd6f5e9 feat(windows): Phase-1 native install + scheduled-task service (#438)
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 <noreply@anthropic.com>
2026-05-29 16:44:10 -07:00

300 lines
11 KiB
PowerShell

<#
.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 ""