# Raven one-line installer for native Windows PowerShell.
#
# Remote:
# irm https://raven.evermind.ai/install.ps1 | iex
#
# A piped run always installs the published release wheel, even from inside a
# clone. Set RAVEN_LOCAL_SRC=
to force an editable install of a checkout.
# Set RAVEN_MINIMAL=1 to skip the chromium download and the LibreOffice offer;
# the wheel install itself is unchanged. Set RAVEN_NO_LAUNCH=1 to skip the
# closing `raven web` (CI), so the script returns.
#
# Goal: a clean Windows machine ends up able to run `raven` / `raven tui`
# without admin rights. The script is idempotent: it reuses existing tools when
# available and only fills the gaps:
# 1. uv (Python toolchain + package manager)
# 2. Node.js >= 22 (TUI runtime; installed privately if the system lacks it)
# 3. raven (installed as a global uv tool)
# 4. chromium (browser-tool runtime; downloaded by playwright)
# 5. LibreOffice (deck preview; offered via winget)
#
# It then ends in the product: `raven web` opens the page in a browser and holds
# this session, so the install finishes on something running rather than on a
# hint to go and start it.
#
# Probe rule: this script is served from main and installs the latest release,
# which can predate a subcommand main already knows about. Every `raven `
# call below is preceded by `raven --help`; when the probe fails, the
# script finishes on the one command every release has.
$ErrorActionPreference = "Stop"
$MinNodeMajor = 22
$RavenHome = if ($env:RAVEN_HOME) { $env:RAVEN_HOME } else { Join-Path $HOME ".raven" }
$NodeRuntimeDir = Join-Path $RavenHome "runtime"
function Write-Info([string]$Message) {
Write-Host ">" $Message -ForegroundColor Cyan
}
function Write-Ok([string]$Message) {
Write-Host "OK" $Message -ForegroundColor Green
}
function Write-Warn([string]$Message) {
Write-Warning $Message
}
function Fail([string]$Message) {
Write-Error $Message
exit 1
}
function Add-ProcessPath([string]$PathToAdd) {
if (-not $PathToAdd) { return }
if (-not (Test-Path $PathToAdd)) { return }
$parts = $env:PATH -split ';'
if ($parts -notcontains $PathToAdd) {
$env:PATH = "$PathToAdd;$env:PATH"
}
}
function Find-Uv {
$cmd = Get-Command uv -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$candidates = @(
(Join-Path $HOME ".local\bin\uv.exe"),
(Join-Path $env:USERPROFILE ".local\bin\uv.exe")
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) { return $candidate }
}
return $null
}
function Ensure-Uv {
$uv = Find-Uv
if ($uv) {
Write-Ok "uv is installed ($(& $uv --version))"
Add-ProcessPath (Split-Path $uv -Parent)
return $uv
}
Write-Info "uv not found; installing..."
Invoke-Expression (Invoke-RestMethod "https://astral.sh/uv/install.ps1")
$uv = Find-Uv
if (-not $uv) {
Fail "uv was installed but is still not available. Check PATH (expected ~/.local/bin)."
}
Add-ProcessPath (Split-Path $uv -Parent)
Write-Ok "uv installed"
return $uv
}
function Get-NodeArch {
switch ($env:PROCESSOR_ARCHITECTURE) {
"ARM64" { return "arm64" }
"AMD64" { return "x64" }
default { Fail "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE" }
}
}
function Test-NodeOk([string]$NodePath) {
if (-not $NodePath) { return $false }
if (-not (Test-Path $NodePath)) { return $false }
try {
$version = (& $NodePath --version).Trim()
$major = [int](($version.TrimStart("v") -split "\.")[0])
return $major -ge $MinNodeMajor
} catch {
return $false
}
}
function Find-PrivateNode {
$candidates = @()
$direct = Join-Path $NodeRuntimeDir "node\node.exe"
$directBin = Join-Path $NodeRuntimeDir "node\bin\node.exe"
if (Test-Path $direct) { $candidates += $direct }
if (Test-Path $directBin) { $candidates += $directBin }
if (Test-Path $NodeRuntimeDir) {
$candidates += Get-ChildItem $NodeRuntimeDir -Directory -Filter "node-v22*" -ErrorAction SilentlyContinue |
ForEach-Object {
@(
(Join-Path $_.FullName "node.exe"),
(Join-Path $_.FullName "bin\node.exe")
)
}
}
foreach ($candidate in $candidates) {
if (Test-NodeOk $candidate) { return $candidate }
}
return $null
}
function Get-LatestNodeV22 {
try {
$index = Invoke-RestMethod "https://nodejs.org/dist/index.json"
$entry = $index | Where-Object { $_.version -like "v22.*" } | Select-Object -First 1
if ($entry -and $entry.version) { return $entry.version }
} catch {
Write-Warn "Could not query Node.js release index; falling back to v22.20.0"
}
return "v22.20.0"
}
function Ensure-Node {
$systemNode = Get-Command node -ErrorAction SilentlyContinue
if ($systemNode -and (Test-NodeOk $systemNode.Source)) {
Write-Ok "Node.js meets requirements ($(& $systemNode.Source --version))"
return $systemNode.Source
}
$privateNode = Find-PrivateNode
if ($privateNode) {
Write-Ok "Existing Raven private Node found ($privateNode)"
Add-ProcessPath (Split-Path $privateNode -Parent)
return $privateNode
}
Write-Info "Node.js >= $MinNodeMajor not found; downloading private runtime..."
return Install-PrivateNode
}
# Download, verify and extract a private Node runtime into $NodeRuntimeDir, and
# return the node.exe in it. Split out of Ensure-Node because Build-WebAssets
# needs it on a second path: a node packaged without npm satisfies Ensure-Node
# and leaves the build with no npm to call. Failures here raise (via Fail), so a
# caller that must not abort on a failed download wraps this in try/catch.
function Install-PrivateNode {
$arch = Get-NodeArch
$version = Get-LatestNodeV22
$pkg = "node-$version-win-$arch"
$url = "https://nodejs.org/dist/$version/$pkg.zip"
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("raven-node-" + [guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $tmp "node.zip"
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
New-Item -ItemType Directory -Path $NodeRuntimeDir -Force | Out-Null
try {
Write-Info " $url"
Invoke-WebRequest $url -OutFile $zipPath
try {
$sums = (Invoke-WebRequest "https://nodejs.org/dist/$version/SHASUMS256.txt").Content
$line = ($sums -split "`n") | Where-Object { $_ -match "\s+$([regex]::Escape("$pkg.zip"))$" } | Select-Object -First 1
if ($line) {
$expected = (($line.Trim()) -split "\s+")[0].ToLowerInvariant()
$actual = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($expected -ne $actual) {
Fail "Node checksum mismatch (expected $expected, got $actual)."
}
Write-Ok "Node zip SHA256 verified"
} else {
Write-Warn "SHASUMS256.txt did not list $pkg.zip; skipping checksum verification"
}
} catch {
Write-Warn "Could not verify Node checksum; continuing"
}
Expand-Archive $zipPath -DestinationPath $tmp -Force
$src = Join-Path $tmp $pkg
$dest = Join-Path $NodeRuntimeDir $pkg
if (Test-Path $dest) { Remove-Item $dest -Recurse -Force }
Move-Item $src $dest
$node = Join-Path $dest "node.exe"
if (-not (Test-NodeOk $node)) {
Fail "Downloaded Node runtime is not usable on this machine."
}
Add-ProcessPath $dest
Write-Ok "Node private runtime ready: $dest"
return $node
} finally {
if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Reads the latest stable tag off the release page redirect. The GitHub API caps
# unauthenticated callers at 60 requests/hour per IP, which a shared egress can
# exhaust; the release page carries no API quota. Returns "" when the redirect is
# missing or does not name a stable tag, so the caller can fail with its own message.
function Resolve-RavenLatestVersion {
$target = ""
try {
$response = Invoke-WebRequest "https://github.com/EverMind-AI/Raven/releases/latest" -MaximumRedirection 0 -UseBasicParsing -ErrorAction Stop
$target = [string]$response.Headers.Location
} catch {
# Windows PowerShell raises on an unfollowed redirect; the Location header
# still rides on the exception's response.
$failed = $_.Exception.Response
if ($failed) {
try { $target = [string]$failed.Headers.Location } catch { $target = "" }
if (-not $target) {
try { $target = [string]$failed.Headers.GetValues("Location")[0] } catch { $target = "" }
}
}
}
if ($target -match "^https://github\.com/EverMind-AI/Raven/releases/tag/v([0-9]+\.[0-9]+\.[0-9]+)$") {
return $Matches[1]
}
return ""
}
function Resolve-RavenWheel {
# One discovery step, no GitHub API: the release page redirect names the
# latest stable tag, and the wheel, the locked constraints and the plugin
# list are all derived from it, since they sit in one release directory.
if ($env:RAVEN_WHEEL_URL) { return $env:RAVEN_WHEEL_URL }
Write-Info "Resolving the latest Raven release from GitHub..."
$version = Resolve-RavenLatestVersion
if (-not $version) {
Fail "Could not resolve the latest Raven release wheel from GitHub. Retry later, or set RAVEN_WHEEL_URL to a wheel URL."
}
return "https://github.com/EverMind-AI/Raven/releases/download/v$version/raven-$version-py3-none-any.whl"
}
function Resolve-RavenPluginList([string]$WheelUrl) {
# The plugin list from the same release directory: the wheels the release
# ships beside raven, one `name @ url` line each, written by the release
# workflow from what it built. What a complete install is made of lives
# there, not here, and `raven upgrade` installs from the same file. Returns
# a local temp-file path, or $null when the release carries none (0.1.13
# and older), which installs raven alone as it always did.
if ($WheelUrl -notmatch "/[^/]+\.whl$") { return $null }
$url = $WheelUrl -replace "/[^/]+\.whl$", "/raven-plugins.txt"
$dest = Join-Path ([IO.Path]::GetTempPath()) ("raven-plugins-" + [guid]::NewGuid().ToString("N") + ".txt")
try {
Invoke-WebRequest $url -OutFile $dest
} catch {
return $null
}
if (-not (Get-Content $dest | Where-Object { $_.Trim() })) { return $null }
return $dest
}
function Resolve-RavenConstraints([string]$WheelUrl) {
# Derive the locked-constraints URL from the wheel URL (same release dir) so
# the constraints always match the wheel being installed -- including when
# RAVEN_WHEEL_URL pins an older wheel. Returns a local temp-file path, or
# $null when the asset is absent (release predates it) or the download fails,
# so the installer degrades to an unconstrained install rather than failing.
$url = $env:RAVEN_CONSTRAINTS_URL
if (-not $url) {
if ($WheelUrl -notmatch "/[^/]+\.whl$") { return $null }
$url = $WheelUrl -replace "/[^/]+\.whl$", "/raven-constraints.txt"
}
$dest = Join-Path ([IO.Path]::GetTempPath()) ("raven-constraints-" + [guid]::NewGuid().ToString("N") + ".txt")
try {
Invoke-WebRequest $url -OutFile $dest
} catch {
Write-Warn "Could not download locked constraints; installing without version pinning."
return $null
}
return $dest
}
function Test-RavenSource([string]$Dir) {
if (-not $Dir) { return $false }
$pyproject = Join-Path $Dir "pyproject.toml"
return (Test-Path $pyproject) -and (Select-String -Path $pyproject -Pattern '^name = "raven"' -Quiet)
}
# True when the artifact is missing, or any source under the named directories
# is newer than it. Missing is not the only reason to build: the install is
# editable, so Python tracks the checkout with no further help, but these two
# artifacts are compiled and nothing relinks them -- checked only for existence
# they keep serving whatever the tree held at first install while every .py
# beside them moves on. mtime is the right question: git stamps every file it
# rewrites with the time it wrote it, so a pull that touched the frontend sorts
# after the artifact and one that did not leaves it alone.
#
# The prune is one level deep, where node_modules, dist and .modern actually
# sit; install.sh prunes them at any depth. Deeper is reachable here only by
# walking node_modules to decide to ignore it, which is the one directory worth
# not walking.
function Test-AssetStale([string]$Artifact, [string[]]$Sources) {
if (-not (Test-Path $Artifact)) { return $true }
$builtAt = (Get-Item $Artifact).LastWriteTimeUtc
$pruned = @("node_modules", "dist", ".modern")
foreach ($dir in $Sources) {
if (-not (Test-Path $dir)) { continue }
if (-not (Test-Path $dir -PathType Container)) {
if ((Get-Item $dir).LastWriteTimeUtc -gt $builtAt) { return $true }
continue
}
$entries = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue |
Where-Object { -not ($_.PSIsContainer -and $pruned -contains $_.Name) })
foreach ($entry in $entries) {
if (-not $entry.PSIsContainer) {
if ($entry.LastWriteTimeUtc -gt $builtAt) { return $true }
continue
}
$newer = Get-ChildItem -LiteralPath $entry.FullName -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { -not $_.PSIsContainer -and $_.LastWriteTimeUtc -gt $builtAt } |
Select-Object -First 1
if ($newer) { return $true }
}
}
return $false
}
# ui-tui\dist\entry.js (the TUI bundle) and ui\dist\index.html (the page
# `raven web` serves) are both gitignored build artifacts. A release wheel
# carries them; an editable install of a checkout gets neither, so without this
# a clone install has no TUI and no page. Both must exist, and both must be no
# older than the sources they were built from, before first run.
function Build-WebAssets([string]$ScriptDir, [string]$NodePath, [string]$UvPath) {
$needTui = Test-AssetStale (Join-Path $ScriptDir "ui-tui\dist\entry.js") @((Join-Path $ScriptDir "ui-tui"))
# The page inlines the shared catalogue (ui-web/build.py reads
# i18n/messages.json), so a catalogue-only change is a page change.
$needPage = Test-AssetStale (Join-Path $ScriptDir "ui-web\dist\index.html") @(
(Join-Path $ScriptDir "ui-web"),
(Join-Path $ScriptDir "i18n")
)
if (-not ($needTui -or $needPage)) { return }
# One probe for both builds. npm ships alongside node, but verify it
# explicitly rather than assume -- a node packaged without npm satisfies
# Ensure-Node, so the build can arrive here with no npm and no private
# runtime ever fetched. The official archive carries npm beside node, so
# fetch one at that point rather than skip both builds on a machine one
# download away from running them. try/catch because that fetch raises on
# failure and this caller must not: the node already here still runs
# `raven tui`, so a failed download is a skipped build, not a failed
# install.
Add-ProcessPath (Split-Path $NodePath -Parent)
$npm = Get-Command npm -ErrorAction SilentlyContinue
if (-not $npm) {
$privateNode = Find-PrivateNode
if (-not $privateNode) {
Write-Info "Found node but not npm; fetching a private Node runtime that carries both..."
try { $privateNode = Install-PrivateNode } catch { $privateNode = $null }
}
if ($privateNode) {
Add-ProcessPath (Split-Path $privateNode -Parent)
$npm = Get-Command npm -ErrorAction SilentlyContinue
}
}
if (-not $npm) {
if ($needTui) { Write-Warn "Found node but not npm; skipping TUI bundle build" }
if ($needPage) { Write-Warn "Found node but not npm; skipping served-page build; raven web will not start" }
return
}
if ($needTui) {
Write-Info "Building TUI bundle (ui-tui/dist/entry.js)..."
Push-Location (Join-Path $ScriptDir "ui-tui")
# Fatal, and each exit code read back for the same reason the page below
# reads its own: $ErrorActionPreference does not cover native commands,
# so an unchecked npm failure returns here, runs the next npm anyway, and
# the install goes on to report success with no bundle -- leaving bare
# `raven`, which opens the TUI, unusable. install.sh aborts here under
# `set -e`; this is the same abort, and `Fail` raises a terminating error
# rather than exiting the process, so `irm | iex` does not close the
# caller's shell and Pop-Location still runs.
try {
& $npm.Source ci
if ($LASTEXITCODE -ne 0) { Fail "TUI bundle build failed: npm ci exited $LASTEXITCODE" }
& $npm.Source run build
if ($LASTEXITCODE -ne 0) { Fail "TUI bundle build failed: npm run build exited $LASTEXITCODE" }
} finally {
Pop-Location
}
}
if ($needPage) {
Write-Info "Building served page (ui-web/dist/index.html)..."
# Warned rather than propagated, unlike the bundle above: bare `raven`
# opens the TUI, so a machine that cannot build the page still gets the
# surface this script exists to deliver. Each exit code is read back
# because $ErrorActionPreference does not cover native commands, so a
# failed npm ci would otherwise run on into the page assembler.
try {
Push-Location (Join-Path $ScriptDir "ui-web")
try {
& $npm.Source ci
if ($LASTEXITCODE -ne 0) { throw "npm ci exited $LASTEXITCODE" }
& $npm.Source run build
if ($LASTEXITCODE -ne 0) { throw "npm run build exited $LASTEXITCODE" }
} finally {
Pop-Location
}
# Vite emits ui-web/.modern/modern.iife.js, then ui-web/build.py inlines
# with the page sources and the shared i18n catalogue. Python comes
# from uv, already a hard requirement here, rather than from a bare
# `python` -- on Windows that name is usually the Microsoft Store
# stub, which opens the Store instead of running the script.
& $UvPath run --no-project python (Join-Path $ScriptDir "ui-web\build.py")
if ($LASTEXITCODE -ne 0) { throw "ui-web/build.py exited $LASTEXITCODE" }
} catch {
Write-Warn "The served page did not build ($_); raven web will not start"
}
}
}
function Install-Raven([string]$UvPath, [string]$NodePath) {
# $PSScriptRoot is set only when this script runs as a file. Piped through
# `irm ... | iex` it is empty, and falling back to the current directory
# turns a one-line install started from inside a clone into a silent
# editable install of that working tree. So local mode requires
# $PSScriptRoot; RAVEN_LOCAL_SRC is the explicit opt-in for a piped run.
$scriptDir = $null
if ($env:RAVEN_LOCAL_SRC) {
$resolved = Resolve-Path -LiteralPath $env:RAVEN_LOCAL_SRC -ErrorAction SilentlyContinue
if (-not $resolved) { Fail "RAVEN_LOCAL_SRC is not a directory: $($env:RAVEN_LOCAL_SRC)" }
$scriptDir = $resolved.Path
if (-not (Test-RavenSource $scriptDir)) { Fail "RAVEN_LOCAL_SRC is not a Raven source checkout: $scriptDir" }
} elseif ($PSScriptRoot -and (Test-RavenSource $PSScriptRoot)) {
$scriptDir = $PSScriptRoot
}
if ($scriptDir) {
Write-Info "Detected local Raven source checkout; installing editable: $scriptDir"
Build-WebAssets $scriptDir $NodePath $UvPath
# Pin to the locked dependency set so an install matches what we test.
$constraints = Join-Path ([IO.Path]::GetTempPath()) ("raven-constraints-" + [guid]::NewGuid().ToString("N") + ".txt")
& $UvPath export --directory "$scriptDir" --frozen --all-extras --no-hashes --no-emit-workspace -o "$constraints"
# The default config names the everos memory backend, which ships as
# its own distribution beside the wheel -- carry it, and degrade
# loudly (the host boots memoryless and `raven doctor` says why) if
# it cannot build.
$memoryPlugin = @(
"--with-editable", (Join-Path $scriptDir "plugins-dist\everos-memory")
)
# Raven-Design and Raven-PPT keep their harness in their own
# distributions, and the roster gates on them: discovery reads the
# `engine` block in each agents//subagent.json and disables the
# row when that package is not importable where raven runs. Without
# these two the products are listed and cannot be dispatched to.
$enginePlugins = @(
"--with-editable", (Join-Path $scriptDir "plugins-dist\design-engine"),
"--with-editable", (Join-Path $scriptDir "plugins-dist\ppt-engine")
)
# Install all channel adapters by default; fall back to base raven if
# the umbrella extra fails to build on this platform, so one broken
# channel SDK cannot block the whole install. Four rungs, dropping one
# capability each: the engines carry native builds (PDF, raster,
# plotting) a platform can refuse on its own, so they fall before the
# memory plugin rather than taking it down with them.
try {
& $UvPath tool install --force -c "$constraints" @memoryPlugin @enginePlugins -e "$scriptDir[channels]"
if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" }
} catch {
Write-Warn "Channel dependencies failed to install; retrying with base raven. Some channels stay unavailable (see: raven channels list)."
& $UvPath tool install --force -c "$constraints" @memoryPlugin @enginePlugins -e "$scriptDir"
if ($LASTEXITCODE -ne 0) {
Write-Warn "A product engine failed to build; Raven-Design and Raven-PPT stay disabled (raven doctor explains)."
& $UvPath tool install --force -c "$constraints" @memoryPlugin -e "$scriptDir"
if ($LASTEXITCODE -ne 0) {
Write-Warn "EverOS memory plugin failed to install; long-term memory stays off (raven doctor explains)."
& $UvPath tool install --force -c "$constraints" -e "$scriptDir"
if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." }
}
}
}
} else {
$wheelUrl = Resolve-RavenWheel
$constraints = Resolve-RavenConstraints $wheelUrl
if ($constraints) {
$cArgs = @("-c", $constraints)
} else {
Write-Warn "Release has no locked-constraints asset; installing without version pinning."
$cArgs = @()
}
$pArgs = @()
$mArgs = @()
$plugins = Resolve-RavenPluginList $wheelUrl
if ($plugins) {
Write-Info " with the release's plugins:"
Get-Content $plugins | ForEach-Object { Write-Info " $_" }
$pArgs = @("--with-requirements", $plugins)
$allLines = @(Get-Content $plugins | Where-Object { $_.Trim() })
$memoryLines = @($allLines | Where-Object { $_ -match "^everos-memory " })
if ($memoryLines.Count -gt 0 -and $memoryLines.Count -lt $allLines.Count) {
$memoryOnly = Join-Path ([IO.Path]::GetTempPath()) ("raven-plugins-memory-" + [guid]::NewGuid().ToString("N") + ".txt")
Set-Content -Path $memoryOnly -Value $memoryLines -Encoding ascii
$mArgs = @("--with-requirements", $memoryOnly)
}
} else {
Write-Warn "This release carries no plugin list; long-term memory, Raven-Design and Raven-PPT stay off (raven doctor explains)."
}
Write-Info " installing $wheelUrl"
# Two independent things can fail: the channel extras, and the plugins
# (the engines' native builds first, the memory plugin after). A failed
# attempt does not say which, so the rungs walk both axes and stop at
# the first that lands -- the largest install this machine can build --
# and warn about exactly what that rung lacks:
# 1 channels + all plugins 4 base + memory plugin
# 2 base + all plugins 5 channels, no plugins
# 3 channels + memory plugin 6 base, no plugins
$lostChannels = "Channel dependencies failed to install; some channels stay unavailable (see: raven channels list)."
$lostEngines = "A product engine failed to install; Raven-Design and Raven-PPT stay disabled (raven doctor explains)."
$lostPlugins = "No plugin could be installed; long-term memory, Raven-Design and Raven-PPT stay off (raven doctor explains)."
$landed = $null
& $UvPath tool install --force @cArgs @pArgs "raven[channels] @ $wheelUrl"
if ($LASTEXITCODE -eq 0) { $landed = @() }
if ($null -eq $landed) {
& $UvPath tool install --force @cArgs @pArgs $wheelUrl
if ($LASTEXITCODE -eq 0) { $landed = @($lostChannels) }
}
if ($null -eq $landed -and $mArgs.Count -gt 0) {
& $UvPath tool install --force @cArgs @mArgs "raven[channels] @ $wheelUrl"
if ($LASTEXITCODE -eq 0) { $landed = @($lostEngines) }
}
if ($null -eq $landed -and $mArgs.Count -gt 0) {
& $UvPath tool install --force @cArgs @mArgs $wheelUrl
if ($LASTEXITCODE -eq 0) { $landed = @($lostEngines, $lostChannels) }
}
if ($null -eq $landed -and $pArgs.Count -gt 0) {
& $UvPath tool install --force @cArgs "raven[channels] @ $wheelUrl"
if ($LASTEXITCODE -eq 0) { $landed = @($lostPlugins) }
}
if ($null -eq $landed -and $pArgs.Count -gt 0) {
& $UvPath tool install --force @cArgs $wheelUrl
if ($LASTEXITCODE -eq 0) { $landed = @($lostPlugins, $lostChannels) }
}
if ($null -eq $landed) { Fail "Raven install failed." }
foreach ($loss in $landed) { Write-Warn $loss }
}
& $UvPath tool update-shell | Out-Null
Write-Ok "Raven installed"
}
# Both optional installs are best-effort: raven itself is already installed by
# the time they run, so a failed download or a declined offer must never abort
# a completed install. RAVEN_MINIMAL skips both.
function Install-Browser([string]$UvPath) {
# The browser tool drives chromium through the playwright library inside
# the raven tool venv, so both the probe and the download must use that
# venv's python -- the system python knows nothing about this install.
$toolDir = ""
try { $toolDir = [string](& $UvPath tool dir 2>$null) } catch { $toolDir = "" }
$py = if ($toolDir) { Join-Path $toolDir "raven\Scripts\python.exe" } else { $null }
if (-not $py -or -not (Test-Path $py)) {
Write-Warn "raven tool venv python not found; skipping the chromium download."
return
}
# A pinned RAVEN_WHEEL_URL and the release-page fallback install no engine
# wheels, so playwright can be absent even after a green install. Windows
# PowerShell turns redirected native stderr into a terminating error under
# $ErrorActionPreference = "Stop", so a failed import lands in the catch.
try {
& $py -c "import playwright" 2>$null
if ($LASTEXITCODE -ne 0) { throw "playwright is not importable" }
} catch {
Write-Warn "This install carries no browser library (a pinned wheel URL or the release-page fallback installs no engines); the browser tool stays off."
return
}
Write-Info "Downloading chromium for the browser tool..."
try {
& $py -m playwright install chromium
if ($LASTEXITCODE -ne 0) { throw "playwright install chromium exited $LASTEXITCODE" }
} catch {
Write-Warn "Chromium download failed; the browser tool stays off. Retry later with: $py -m playwright install chromium"
}
}
function Install-Office {
# soffice and libreoffice are the two launcher names the runtime resolves
# (raven/utils/office.py); either one means deck preview already works. The
# winget MSI registers no PATH entry, so also probe the install roots
# find_soffice reads before deciding LibreOffice is absent.
if (Get-Command soffice, libreoffice -ErrorAction SilentlyContinue) { return }
foreach ($root in @($env:ProgramFiles, ${env:ProgramW6432}, ${env:ProgramFiles(x86)}, $env:LOCALAPPDATA)) {
if ($root -and (Test-Path (Join-Path $root "LibreOffice\program\soffice.exe"))) { return }
}
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Warn "LibreOffice not found; deck preview stays off. Install it later with: winget install TheDocumentFoundation.LibreOffice"
return
}
# Installing can raise a UAC prompt, so ask first -- and only when a real
# console is attached: under `irm | iex` Read-Host still reads the console,
# but CI has none, and a prompt there must skip cleanly, never hang.
if (-not ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected)) {
Write-Warn "LibreOffice not found; deck preview stays off. Install it later with: winget install TheDocumentFoundation.LibreOffice"
return
}
# Default yes, matching install.sh: for the deck lane this is the one
# dependency that matters, and macOS installs it without asking.
$answer = Read-Host "Install LibreOffice for deck preview (may raise a UAC prompt)? Without it a deck still builds, but no page is ever rendered, measured or checked. [Y/n]"
if ($answer -match "^[nN]") {
Write-Warn "Skipping LibreOffice; deck preview stays off. Install it later with: winget install TheDocumentFoundation.LibreOffice"
return
}
try {
winget install TheDocumentFoundation.LibreOffice
if ($LASTEXITCODE -ne 0) { throw "winget exited $LASTEXITCODE" }
} catch {
Write-Warn "LibreOffice install failed; deck preview stays off. Retry later with: winget install TheDocumentFoundation.LibreOffice"
}
}
# The install ends on a running page. `--stop` first, because a gateway an
# earlier install left resident would be attached to instead of the build that
# just landed; `--foreground` then holds this session on a fresh one and opens
# the browser on it, so Ctrl-C here means what it says. The raven shim lands in
# `uv tool dir --bin`, which this session's PATH may not carry yet, so invoke it
# by absolute path.
#
# A non-zero page exit only warns: under `irm | iex` this is the caller's own
# interactive PowerShell, and Ctrl-C -- the ordinary way to end a foreground
# page -- comes back non-zero, so exiting on it would close the window the
# reader is standing in.
function Start-Web([string]$UvPath) {
$binDir = ""
try { $binDir = [string](& $UvPath tool dir --bin 2>$null) } catch { $binDir = "" }
$bin = if ($binDir) { Join-Path $binDir "raven.exe" } else { $null }
if (-not $bin -or -not (Test-Path $bin)) { $bin = Join-Path $HOME ".local\bin\raven.exe" }
if (-not (Test-Path $bin)) {
Write-Warn "raven is not where this script looked for it; open a new PowerShell window and run: raven"
return
}
# The release this script just installed may predate `raven web` (0.1.13
# does). Ask before calling, and end on the command every release has.
& $bin web --help *> $null
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Ok "Raven installed. Open a new PowerShell window, then run: raven"
return
}
Write-Host ""
Write-Ok "Starting Raven -- your browser will open in a moment. Ctrl-C here stops it."
Write-Host ""
# `raven web` starts its engine as `python -m raven`, which puts the working
# directory first on sys.path: run from inside a source checkout, a release
# without the `-P` guard comes up on that checkout's raven, and the published
# release this script installs may predate that guard. Under `irm | iex` this
# is the caller's own shell, so the directory is put back once the page ends.
Push-Location $HOME
try {
& $bin web --stop *> $null
if ($LASTEXITCODE -ne 0) { Write-Warn "could not stop a previous gateway; continuing" }
& $bin web --foreground
if ($LASTEXITCODE -ne 0) {
Write-Warn "the page ended with exit code $LASTEXITCODE; start it again with 'raven web'"
}
} finally {
Pop-Location
}
}
function Main {
$uv = Ensure-Uv
$node = Ensure-Node
Install-Raven $uv $node
if (-not $env:RAVEN_MINIMAL) { Install-Browser $uv }
if (-not $env:RAVEN_MINIMAL) { Install-Office }
# Before the page, not after: the page holds this session until Ctrl-C, and
# `raven` has to work in the session the reader comes back to.
Add-ProcessPath (Join-Path $HOME ".local\bin")
if (-not $env:RAVEN_NO_LAUNCH) { Start-Web $uv }
}
Main