# OpenCaddis installer for 64-bit Windows 10 1809 and newer. # Usage: iwr -useb https://opencaddis.ai/install.ps1 | iex param( [ValidatePattern('^(latest|v\d+\.\d+\.\d+)$')] [string]$Version = 'latest', [string]$InstallDir, [switch]$NoLaunch, [switch]$Uninstall, [switch]$PurgeData, # The remaining parameters support controlled installs and automated testing. [string]$PackagePath, [string]$ChecksumPath, [string]$DataDir, [string]$ShortcutDir, [switch]$SkipWebView2, [switch]$TestFailAfterBackup ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 $script:ProductName = 'OpenCaddis' $script:ExecutableName = 'OpenCaddis.App.exe' $script:AssetName = 'OpenCaddis-win-x64.zip' $script:Repository = 'vulcan365/OpenCaddis' $script:WebView2ProductId = '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' $script:WebView2BootstrapperUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan } function Write-Success { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green } function Get-NormalizedPath { param([string]$Path) $expanded = [Environment]::ExpandEnvironmentVariables($Path) return [IO.Path]::GetFullPath($expanded).TrimEnd('\') } function Assert-SafeDirectory { param( [string]$Path, [string]$Description ) if ([string]::IsNullOrWhiteSpace($Path)) { throw "$Description cannot be empty." } $root = [IO.Path]::GetPathRoot($Path) if ($Path.TrimEnd('\') -eq $root.TrimEnd('\')) { throw "$Description cannot be a drive root." } } function Test-InstalledAppRunning { param([string]$ExpectedInstallDir) $processes = @(Get-Process -Name 'OpenCaddis.App' -ErrorAction SilentlyContinue) foreach ($process in $processes) { $processPath = $null try { $processPath = $process.Path } catch { try { $processPath = $process.MainModule.FileName } catch { } } if ([string]::IsNullOrWhiteSpace($processPath)) { return $true } $normalizedProcessPath = Get-NormalizedPath -Path $processPath if ($normalizedProcessPath.StartsWith($ExpectedInstallDir + '\', [StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function Get-ShortcutPath { param([string]$Directory) return Join-Path $Directory 'OpenCaddis.lnk' } function Remove-FileWithRetry { param( [string]$Path, [int]$Attempts = 10 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { try { Remove-Item -LiteralPath $Path -Force -ErrorAction Stop return } catch { if ($attempt -eq $Attempts) { throw } Start-Sleep -Milliseconds 250 } } } function New-OpenCaddisShortcut { param( [string]$Directory, [string]$TargetPath ) New-Item -ItemType Directory -Force -Path $Directory | Out-Null $shell = New-Object -ComObject WScript.Shell $shortcut = $shell.CreateShortcut((Get-ShortcutPath -Directory $Directory)) $shortcut.TargetPath = $TargetPath $shortcut.WorkingDirectory = Split-Path -Parent $TargetPath $shortcut.IconLocation = "$TargetPath,0" $shortcut.Description = 'OpenCaddis local AI agent workspace' $shortcut.Save() } function Test-WebView2Runtime { $registryPaths = @( "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\$($script:WebView2ProductId)", "HKCU:\Software\Microsoft\EdgeUpdate\Clients\$($script:WebView2ProductId)" ) foreach ($registryPath in $registryPaths) { try { $versionValue = (Get-ItemProperty -LiteralPath $registryPath -Name 'pv' -ErrorAction Stop).pv if (-not [string]::IsNullOrWhiteSpace($versionValue) -and $versionValue -ne '0.0.0.0') { return $true } } catch { } } return $false } function Install-WebView2Runtime { param([string]$TempDirectory) if (Test-WebView2Runtime) { Write-Success 'Microsoft Edge WebView2 Runtime is available.' return } Write-Step 'Installing the Microsoft Edge WebView2 Runtime for the current user...' $bootstrapperPath = Join-Path $TempDirectory 'MicrosoftEdgeWebview2Setup.exe' Invoke-WebRequest -UseBasicParsing -Uri $script:WebView2BootstrapperUrl -OutFile $bootstrapperPath $process = Start-Process -FilePath $bootstrapperPath -ArgumentList '/silent', '/install' -Wait -PassThru if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { throw "The WebView2 Runtime installer failed with exit code $($process.ExitCode)." } if (-not (Test-WebView2Runtime)) { throw 'The WebView2 Runtime installer completed, but the runtime could not be detected.' } Write-Success 'Microsoft Edge WebView2 Runtime installed.' } function Get-ReleaseAssetUrl { param( [string]$RequestedVersion, [string]$Asset ) if ($RequestedVersion -eq 'latest') { return "https://github.com/$($script:Repository)/releases/latest/download/$Asset" } return "https://github.com/$($script:Repository)/releases/download/$RequestedVersion/$Asset" } function Get-ExpectedChecksum { param([string]$Path) $content = Get-Content -LiteralPath $Path -Raw $match = [regex]::Match($content, '(?i)\b[0-9a-f]{64}\b') if (-not $match.Success) { throw "The checksum file '$Path' does not contain a SHA-256 value." } return $match.Value.ToLowerInvariant() } function Remove-OpenCaddis { param( [string]$TargetInstallDir, [string]$TargetDataDir, [string]$TargetShortcutDir, [bool]$RemoveData ) if (Test-InstalledAppRunning -ExpectedInstallDir $TargetInstallDir) { throw 'OpenCaddis is running. Close the app and run the uninstall command again.' } $shortcutPath = Get-ShortcutPath -Directory $TargetShortcutDir if (Test-Path -LiteralPath $shortcutPath -PathType Leaf) { Remove-FileWithRetry -Path $shortcutPath } if (Test-Path -LiteralPath $TargetInstallDir -PathType Container) { Remove-Item -LiteralPath $TargetInstallDir -Recurse -Force } if ($RemoveData -and (Test-Path -LiteralPath $TargetDataDir -PathType Container)) { Assert-SafeDirectory -Path $TargetDataDir -Description 'Data directory' Remove-Item -LiteralPath $TargetDataDir -Recurse -Force } Write-Success 'OpenCaddis uninstalled.' if (-not $RemoveData) { Write-Host "User data was preserved at '$TargetDataDir'." -ForegroundColor Gray } } if ($env:OS -ne 'Windows_NT') { throw 'OpenCaddis currently supports Windows only.' } if (-not [Environment]::Is64BitOperatingSystem) { throw 'OpenCaddis requires 64-bit Windows.' } $minimumWindowsVersion = [Version]'10.0.17763.0' if ([Environment]::OSVersion.Version -lt $minimumWindowsVersion) { throw 'OpenCaddis requires Windows 10 version 1809 or newer.' } if ($PurgeData -and -not $Uninstall) { throw '-PurgeData can only be used together with -Uninstall.' } if ([string]::IsNullOrWhiteSpace($InstallDir)) { $InstallDir = Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'OpenCaddis' } if ([string]::IsNullOrWhiteSpace($DataDir)) { $DataDir = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenCaddis') 'OpenCaddis.App') 'Data' } if ([string]::IsNullOrWhiteSpace($ShortcutDir)) { $ShortcutDir = Join-Path (Join-Path (Join-Path $env:APPDATA 'Microsoft') 'Windows\Start Menu') 'Programs' } $InstallDir = Get-NormalizedPath -Path $InstallDir $DataDir = Get-NormalizedPath -Path $DataDir $ShortcutDir = Get-NormalizedPath -Path $ShortcutDir Assert-SafeDirectory -Path $InstallDir -Description 'Install directory' Assert-SafeDirectory -Path $DataDir -Description 'Data directory' Assert-SafeDirectory -Path $ShortcutDir -Description 'Shortcut directory' if ($Uninstall) { Remove-OpenCaddis ` -TargetInstallDir $InstallDir ` -TargetDataDir $DataDir ` -TargetShortcutDir $ShortcutDir ` -RemoveData ([bool]$PurgeData) return } if ([string]::IsNullOrWhiteSpace($PackagePath) -xor [string]::IsNullOrWhiteSpace($ChecksumPath)) { throw '-PackagePath and -ChecksumPath must be supplied together.' } if (Test-InstalledAppRunning -ExpectedInstallDir $InstallDir) { throw 'OpenCaddis is running. Close the app and run the installer again.' } if ([Net.ServicePointManager]::SecurityProtocol -band [Net.SecurityProtocolType]::Tls12) { # TLS 1.2 is already enabled. } else { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } $tempRoot = Join-Path ([IO.Path]::GetTempPath()) ('OpenCaddis-install-' + [Guid]::NewGuid().ToString('N')) $stagingPath = Join-Path $tempRoot 'staging' $backupPath = $InstallDir + '.backup-' + [Guid]::NewGuid().ToString('N') $backupCreated = $false $newInstallCreated = $false try { New-Item -ItemType Directory -Force -Path $tempRoot, $stagingPath | Out-Null if ([string]::IsNullOrWhiteSpace($PackagePath)) { $resolvedPackagePath = Join-Path $tempRoot $script:AssetName $resolvedChecksumPath = Join-Path $tempRoot ($script:AssetName + '.sha256') Write-Step "Downloading OpenCaddis $Version..." Invoke-WebRequest -UseBasicParsing ` -Uri (Get-ReleaseAssetUrl -RequestedVersion $Version -Asset $script:AssetName) ` -OutFile $resolvedPackagePath Invoke-WebRequest -UseBasicParsing ` -Uri (Get-ReleaseAssetUrl -RequestedVersion $Version -Asset ($script:AssetName + '.sha256')) ` -OutFile $resolvedChecksumPath } else { $resolvedPackagePath = (Resolve-Path -LiteralPath $PackagePath).ProviderPath $resolvedChecksumPath = (Resolve-Path -LiteralPath $ChecksumPath).ProviderPath } Write-Step 'Verifying package integrity...' $expectedHash = Get-ExpectedChecksum -Path $resolvedChecksumPath $actualHash = (Get-FileHash -LiteralPath $resolvedPackagePath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualHash -ne $expectedHash) { throw "Package checksum verification failed. Expected $expectedHash but received $actualHash." } Write-Success 'Package checksum verified.' Expand-Archive -LiteralPath $resolvedPackagePath -DestinationPath $stagingPath -Force $stagedExecutable = Join-Path $stagingPath $script:ExecutableName $stagedVersionFile = Join-Path $stagingPath 'version.txt' if (-not (Test-Path -LiteralPath $stagedExecutable -PathType Leaf)) { throw "The package does not contain '$($script:ExecutableName)'." } if (-not (Test-Path -LiteralPath $stagedVersionFile -PathType Leaf)) { throw "The package does not contain 'version.txt'." } $installedVersion = (Get-Content -LiteralPath $stagedVersionFile -Raw).Trim() if ($installedVersion -notmatch '^\d+\.\d+\.\d+$') { throw "The package contains an invalid version '$installedVersion'." } if (-not $SkipWebView2) { Install-WebView2Runtime -TempDirectory $tempRoot } Write-Step "Installing OpenCaddis $installedVersion to '$InstallDir'..." $installParent = Split-Path -Parent $InstallDir New-Item -ItemType Directory -Force -Path $installParent | Out-Null if (Test-Path -LiteralPath $InstallDir -PathType Container) { Move-Item -LiteralPath $InstallDir -Destination $backupPath $backupCreated = $true } if ($TestFailAfterBackup) { throw 'Simulated replacement failure.' } Move-Item -LiteralPath $stagingPath -Destination $InstallDir $newInstallCreated = $true New-OpenCaddisShortcut ` -Directory $ShortcutDir ` -TargetPath (Join-Path $InstallDir $script:ExecutableName) if ($backupCreated -and (Test-Path -LiteralPath $backupPath -PathType Container)) { Remove-Item -LiteralPath $backupPath -Recurse -Force $backupCreated = $false } Write-Success "OpenCaddis $installedVersion installed." } catch { if ($newInstallCreated -and (Test-Path -LiteralPath $InstallDir -PathType Container)) { Remove-Item -LiteralPath $InstallDir -Recurse -Force } if ($backupCreated -and (Test-Path -LiteralPath $backupPath -PathType Container)) { if (Test-Path -LiteralPath $InstallDir) { Remove-Item -LiteralPath $InstallDir -Recurse -Force } Move-Item -LiteralPath $backupPath -Destination $InstallDir $backupCreated = $false Write-Host 'The previous OpenCaddis installation was restored.' -ForegroundColor Yellow } throw } finally { if (Test-Path -LiteralPath $tempRoot -PathType Container) { Remove-Item -LiteralPath $tempRoot -Recurse -Force } } if (-not $NoLaunch) { try { Start-Process -FilePath (Join-Path $InstallDir $script:ExecutableName) | Out-Null } catch { Write-Host "OpenCaddis was installed, but could not be started: $($_.Exception.Message)" -ForegroundColor Yellow } }