# install_source.ps1 - user-local OpenSquilla installer (no admin). # # Installer contract: # - installs into a user-owned prefix (never Program Files or system32) # - prefers uv tool install; falls back to pip --user; errors clearly if neither exists # - defaults to the "recommended" runtime profile (memory + bundled v4 router) # and allows `$env:OPENSQUILLA_INSTALL_PROFILE="core"` to opt back down # - on Windows, best-effort installs Microsoft Visual C++ Redistributable # before the recommended router profile because onnxruntime requires it # - prints a post-install banner documenting the default bind # (127.0.0.1:18791) and the explicit opt-in required to expose the gateway # on the network (-Listen 0.0.0.0 or $env:OPENSQUILLA_LISTEN="0.0.0.0") # - adds an extra WARNING when the operator requested network exposure at # install time via $env:OPENSQUILLA_LISTEN="0.0.0.0" # # Dry-run: set $env:OPENSQUILLA_INSTALL_DRY_RUN="1" to print the install plan + # banner without touching the system. param( [string]$Profile = "", [string[]]$Extras = @() ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # --- prefix resolution ------------------------------------------------------ if ($env:OPENSQUILLA_PREFIX) { $prefix = $env:OPENSQUILLA_PREFIX } elseif ($env:LOCALAPPDATA) { $prefix = Join-Path $env:LOCALAPPDATA 'opensquilla' } else { $prefix = Join-Path $HOME '.local' } $dryRun = $env:OPENSQUILLA_INSTALL_DRY_RUN -eq '1' $script:isWindowsHost = if (Get-Variable IsWindows -ErrorAction SilentlyContinue) { $IsWindows } else { $env:OS -eq 'Windows_NT' } $profile = if ($Profile) { $Profile } elseif ($env:OPENSQUILLA_INSTALL_PROFILE) { $env:OPENSQUILLA_INSTALL_PROFILE } else { 'recommended' } $validExtras = @( 'matrix', 'matrix-e2e', 'document-extras' ) function Split-InstallExtras { param([string[]]$Values) $items = New-Object System.Collections.Generic.List[string] foreach ($value in $Values) { if (-not $value) { continue } foreach ($part in ($value -split '[,\s]+')) { $item = $part.Trim() if ($item -and -not $items.Contains($item)) { $items.Add($item) } } } return $items.ToArray() } $extraInputs = @() if ($env:OPENSQUILLA_INSTALL_EXTRAS) { $extraInputs += $env:OPENSQUILLA_INSTALL_EXTRAS } $extraInputs += $Extras $installExtras = @(Split-InstallExtras $extraInputs) $unknownExtras = @($installExtras | Where-Object { $_ -notin $validExtras }) if ($unknownExtras.Count -gt 0) { Write-Error "install_source.ps1: unsupported extras: $($unknownExtras -join ', '). Supported extras: $($validExtras -join ', ')." exit 1 } switch ($profile) { 'core' { $targetExtras = @() } 'minimal' { $profile = 'core'; $targetExtras = @() } 'recommended' { $targetExtras = @('recommended') } default { Write-Error "install_source.ps1: unsupported OPENSQUILLA_INSTALL_PROFILE='$profile'. Supported profiles: core, recommended." exit 1 } } $targetExtras += $installExtras $installTarget = if ($targetExtras.Count -gt 0) { ".[$($targetExtras -join ',')]" } else { '.' } function Test-SquillaRouterAssets { param( [switch]$WarnOnly ) if ($profile -ne 'recommended') { return } $modelRoot = 'src/opensquilla/squilla_router/models' $required = @( "$modelRoot/v4.2_phase3_inference/lgbm_main.bin", "$modelRoot/v4.2_phase3_inference/router.runtime.yaml", "$modelRoot/v4.2_phase3_inference/mlp/model.onnx", "$modelRoot/v4.2_phase3_inference/features/tfidf.pkl", "$modelRoot/v4.2_phase3_inference/bge_onnx/model.onnx" ) $pointerLine = 'version https://git-lfs.github.com/spec/v1' $missing = New-Object System.Collections.Generic.List[string] $pointers = New-Object System.Collections.Generic.List[string] foreach ($path in $required) { if (-not (Test-Path $path -PathType Leaf)) { $missing.Add($path) continue } $firstLine = Get-Content -Path $path -TotalCount 1 -ErrorAction SilentlyContinue if ($firstLine -eq $pointerLine) { $pointers.Add($path) } } if ($missing.Count -gt 0 -or $pointers.Count -gt 0) { if ($WarnOnly) { Write-Host 'install_source.ps1: dry-run note — real recommended install would fail until bundled squilla-router v4 assets are available in this checkout.' } else { Write-Error 'install_source.ps1: bundled squilla-router v4 assets are unavailable in this checkout.' } if ($missing.Count -gt 0) { $message = "install_source.ps1: missing squilla-router assets: $($missing -join ', ')" if ($WarnOnly) { Write-Host $message } else { Write-Error $message } } if ($pointers.Count -gt 0) { $message = "install_source.ps1: Git LFS pointer files detected: $($pointers -join ', ')" if ($WarnOnly) { Write-Host $message } else { Write-Error $message } } $lfsMessage = 'install_source.ps1: run `git lfs install` once, then `git lfs pull --include="src/opensquilla/squilla_router/models/**"`.' $coreMessage = 'install_source.ps1: or retry with `$env:OPENSQUILLA_INSTALL_PROFILE="core"` for the minimal runtime.' if ($WarnOnly) { Write-Host $lfsMessage Write-Host $coreMessage return } Write-Error $lfsMessage Write-Error $coreMessage exit 1 } } function Test-WindowsVCRedistInstalled { if (-not $script:isWindowsHost) { return $true } $runtimeKeys = @( 'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64' ) foreach ($key in $runtimeKeys) { if (-not (Test-Path $key)) { continue } $runtime = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue if ($runtime -and $runtime.Installed -eq 1 -and $runtime.Major -ge 14) { return $true } } return $false } function Install-WindowsVCRedistIfNeeded { if (-not $script:isWindowsHost -or $profile -ne 'recommended') { return } if ($env:OPENSQUILLA_SKIP_VC_REDIST -eq '1') { Write-Host 'install_source.ps1: skipping Microsoft Visual C++ Redistributable check because OPENSQUILLA_SKIP_VC_REDIST=1.' return } if (Test-WindowsVCRedistInstalled) { Write-Host 'install_source.ps1: Microsoft Visual C++ Redistributable is already installed.' return } $redistUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe' $winget = Get-Command winget -ErrorAction SilentlyContinue if ($winget) { Write-Host 'install_source.ps1: Microsoft Visual C++ Redistributable not detected; installing with winget.' $wingetArgs = @( 'install', '--id', 'Microsoft.VCRedist.2015+.x64', '--exact', '--silent', '--accept-package-agreements', '--accept-source-agreements' ) & winget @wingetArgs if ($LASTEXITCODE -eq 0) { Write-Host 'install_source.ps1: Microsoft Visual C++ Redistributable installation completed.' return } Write-Warning "install_source.ps1: winget could not install Microsoft Visual C++ Redistributable (exit $LASTEXITCODE)." } Write-Warning 'OpenSquilla: Microsoft Visual C++ Redistributable 2015-2022 x64 is required for the bundled ONNX router.' Write-Warning 'OpenSquilla can still start with safe router fallback, but bundled ONNX model routing is disabled until this runtime is installed.' Write-Warning "If automatic installation fails, install it manually: $redistUrl" Write-Warning 'After installing, reopen PowerShell and restart OpenSquilla.' } # --- installer selection ---------------------------------------------------- $installer = $null $installArgs = @() # Probe the ambient python version once (used only for the pip fallback gate). $pythonCmd = Get-Command python -ErrorAction SilentlyContinue $pythonOk = $false if ($pythonCmd) { & python -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' 2>$null $pythonOk = ($LASTEXITCODE -eq 0) } if (Get-Command uv -ErrorAction SilentlyContinue) { $installer = 'uv' $installArgs = @('tool', 'install', '--python', '3.12', '--force', '--reinstall-package', 'opensquilla', $installTarget) } elseif ($pythonOk) { $installer = 'pip' $installArgs = @('-m', 'pip', 'install', '--user', $installTarget) } else { # No uv, and the ambient python is missing or older than 3.12. Do NOT # silently pip-install onto an unsupported interpreter: a broken # opensquilla makes coding mode fall back to manual edits. Fail loud. $pyver = if ($pythonCmd) { (& python -V 2>&1) } else { 'none' } Write-Error "install_source.ps1: cannot install - uv not found and python ($pyver) is older than 3.12. OpenSquilla requires Python >= 3.12. Install uv (it brings its own 3.12): 'irm https://astral.sh/uv/install.ps1 | iex', then re-run scripts/install_source.ps1." exit 1 } $installCmd = if ($installer -eq 'uv') { "uv $($installArgs -join ' ')" } else { "python $($installArgs -join ' ')" } # --- banner ----------------------------------------------------------------- function Write-Banner { @" ---------------------------------------------------------------------------- OpenSquilla installed via $installer -> $prefix (profile: $profile) Extras: $(if ($installExtras.Count -gt 0) { $installExtras -join ', ' } else { 'none' }) Default gateway bind: 127.0.0.1:18791 (loopback only) Network exposure is opt-in only. To expose the gateway on the network you must use one of: - CLI flag: opensquilla gateway run --listen 0.0.0.0 - Env var: `$env:OPENSQUILLA_LISTEN="0.0.0.0"; opensquilla gateway run Reminder: only expose 0.0.0.0 behind a trusted reverse proxy or VPN. The gateway's first-class auth assumes loopback-scope by default. ---------------------------------------------------------------------------- "@ | Write-Host } function Write-ListenWarning { @" WARNING: you have selected network-exposed default - ensure you understand the blast radius. The gateway will bind to 0.0.0.0 and be reachable from every interface on this host. "@ | Write-Host } # --- post-install PATH sanity (parity with install_source.sh) -------------- function Resolve-EntrypointDir { # Determine where the just-installed `opensquilla`/`gateway` entry points # landed, so we can warn when that directory is not on PATH. uv tool # install drops entry points in `uv tool dir --bin`; pip --user puts them # in the interpreter's Scripts dir. Both live outside the default PATH on # a clean Windows host - the exact failure mode `opensquilla onboard` # hits right after a "successful" install. Parity with install_source.sh, # which does the same absolute-path lookup on POSIX. if ($installer -eq 'uv') { $uvBin = $null try { $line = (& uv tool dir --bin 2>$null | Select-Object -First 1) if ($line) { $uvBin = $line.Trim() } } catch { } if ($uvBin -and (Test-Path (Join-Path $uvBin 'opensquilla.exe') -PathType Leaf)) { return $uvBin } $fallback = Join-Path $HOME '.local\bin' if (Test-Path (Join-Path $fallback 'opensquilla.exe') -PathType Leaf) { return $fallback } return $null } else { $scriptsDir = $null try { $line = (& python -c "import sysconfig; print(sysconfig.get_path('scripts'))" 2>$null | Select-Object -First 1) if ($line) { $scriptsDir = $line.Trim() } } catch { } if ($scriptsDir -and (Test-Path (Join-Path $scriptsDir 'opensquilla.exe') -PathType Leaf)) { return $scriptsDir } return $null } } function Test-DirOnUserPath { param([string]$Dir) if (-not $Dir) { return $false } $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not $userPath) { return $false } $target = $Dir.TrimEnd('\') foreach ($entry in ($userPath -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } if ([string]::Equals($entry.TrimEnd('\'), $target, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function Write-PathHint { # Verify the just-installed entry point is reachable from a fresh shell. # install_source.sh runs the same smoke check on POSIX; this brings the # PowerShell installer to parity so a "successful" install does not leave # the user with an unresolvable `opensquilla` command (see issue #500). $entryDir = Resolve-EntrypointDir if (-not $entryDir) { Write-Warning 'install_source.ps1: could not locate the installed `opensquilla` entry point to verify PATH.' Write-Warning "install_source.ps1: if `opensquilla` is not recognized, run 'uv tool update-shell' and open a new terminal." return } if (Test-DirOnUserPath -Dir $entryDir) { Write-Host "install_source.ps1: entry points are on PATH ($entryDir)." return } Write-Warning "install_source.ps1: entry points are NOT on PATH: $entryDir" Write-Warning 'install_source.ps1: `opensquilla` will not be found in a new terminal until this is fixed.' Write-Warning 'install_source.ps1: fix it with one of:' Write-Warning ' uv tool update-shell # uv official PATH configurator (recommended)' $oneLiner = '[Environment]::SetEnvironmentVariable(''Path'', [Environment]::GetEnvironmentVariable(''Path'',''User'') + '';{0}'', ''User'')' -f $entryDir Write-Warning " $oneLiner # or add this dir to user PATH manually" Write-Warning "install_source.ps1: then open a new terminal and run 'opensquilla onboard'." } if ($dryRun) { Write-Host "install_source.ps1: dry-run — would run: $installCmd" Write-Host "install_source.ps1: dry-run — prefix: $prefix" Test-SquillaRouterAssets -WarnOnly Write-Banner if ($env:OPENSQUILLA_LISTEN -eq '0.0.0.0') { Write-ListenWarning } exit 0 } # --- execute --------------------------------------------------------------- Install-WindowsVCRedistIfNeeded Test-SquillaRouterAssets Write-Host "install_source.ps1: installing via $installer into prefix $prefix" Write-Host "install_source.ps1: running: $installCmd" if ($installer -eq 'uv') { & uv @installArgs } else { & python @installArgs } if ($LASTEXITCODE -ne 0) { Write-Error "install_source.ps1: install command failed with exit code $LASTEXITCODE." Write-Error 'install_source.ps1: Close any running OpenSquilla gateway or shell using the existing tool environment, then retry.' exit $LASTEXITCODE } # Write an install receipt to aid `opensquilla uninstall`. Best-effort. try { $receiptHome = if ($env:OPENSQUILLA_STATE_DIR) { $env:OPENSQUILLA_STATE_DIR } else { Join-Path $HOME '.opensquilla' } $receiptMethod = if ($installer -eq 'uv') { 'uv-tool' } else { 'pip' } New-Item -ItemType Directory -Force -Path $receiptHome | Out-Null $receipt = [ordered]@{ version = 1 install_method = $receiptMethod installed_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') entrypoints = @() owned_paths = @() data_root = $receiptHome } $receipt | ConvertTo-Json | Set-Content -Path (Join-Path $receiptHome 'install-receipt.json') -Encoding utf8 } catch { # Receipt is optional; never fail the install over it. } # Smoke-check the just-installed entry point is reachable from a fresh shell. # Runs after install only (dry-run exits above), matching install_source.sh. Write-PathHint Write-Banner if ($env:OPENSQUILLA_LISTEN -eq '0.0.0.0') { Write-ListenWarning }