Private/Start-Installation.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Performs the installation itself, once everything has been resolved. .DESCRIPTION By the time this runs, prerequisites are present and the configuration has been collected and confirmed. It only writes files. The winget auto-install that used to live here has moved to phase 3 of Install-BATCRelayBot. Keeping it here duplicated the copy in the public function and ran after the readiness check that it was supposed to satisfy. #> function Start-Installation { [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [hashtable]$Prerequisites, [Parameter(Mandatory = $true)] [hashtable]$DiscordConfig, [string]$InstallPath = (Join-Path $env:LOCALAPPDATA "BATCRelayBot"), [string]$LogPath ) if (-not $LogPath) { $LogPath = Join-Path $InstallPath "install.log" } $configPath = Join-Path $InstallPath "config.json" Write-Host "" # ---- 1: directory --------------------------------------------------- Write-Host " [1/4] Installation directory" -ForegroundColor Gray try { if (-not (Test-Path $InstallPath)) { New-Item -ItemType Directory -Path $InstallPath -Force -ErrorAction Stop | Out-Null } Write-InstallLog "Installation directory ready: $InstallPath" -LogPath $LogPath Write-Host " OK $InstallPath" -ForegroundColor Green } catch { $message = Remove-SensitiveData -Text $_.Exception.Message Write-InstallLog "Could not create $($InstallPath): $message" -LogPath $LogPath -Level ERROR Write-Host " FAILED: $message" -ForegroundColor Red return @{ Success = $false; Error = "Could not create the installation directory" } } # ---- 2: python dependencies ----------------------------------------- Write-Host " [2/4] Python dependencies" -ForegroundColor Gray $requirementsPath = Get-RequirementsPath if (-not $requirementsPath) { Write-InstallLog "requirements.txt not found in any known location" -LogPath $LogPath -Level ERROR Write-Host " FAILED: requirements.txt not found" -ForegroundColor Red return @{ Success = $false; Error = "requirements.txt not found" } } try { Write-InstallLog "pip install -r $requirementsPath" -LogPath $LogPath $pipOutput = & $Prerequisites.Python.Path -m pip install -r $requirementsPath --disable-pip-version-check 2>&1 | ForEach-Object { "$_" } if ($LASTEXITCODE -ne 0) { foreach ($line in ($pipOutput | Select-Object -Last 10)) { Write-InstallLog "pip: $line" -LogPath $LogPath -Level ERROR } Write-Host " FAILED: pip exited with code $LASTEXITCODE" -ForegroundColor Red Write-Host " See the log for pip's output: $LogPath" -ForegroundColor Yellow return @{ Success = $false; Error = "Installing Python dependencies failed" } } Write-InstallLog "Python dependencies installed" -LogPath $LogPath Write-Host " OK discord.py and dependencies installed" -ForegroundColor Green } catch { $message = Remove-SensitiveData -Text $_.Exception.Message Write-InstallLog "pip failed: $message" -LogPath $LogPath -Level ERROR Write-Host " FAILED: $message" -ForegroundColor Red return @{ Success = $false; Error = "Installing Python dependencies failed" } } # ---- 3: bot files --------------------------------------------------- Write-Host " [3/4] Bot files" -ForegroundColor Gray $botSource = Get-BotFilesPath if (-not $botSource) { Write-InstallLog "bot.py not found in any known location" -LogPath $LogPath -Level ERROR Write-Host " FAILED: bot.py not found" -ForegroundColor Red return @{ Success = $false; Error = "bot.py not found" } } try { Copy-Item -Path (Join-Path $botSource "bot.py") -Destination $InstallPath -Force -ErrorAction Stop Write-InstallLog "bot.py copied from $botSource" -LogPath $LogPath Write-Host " OK bot.py" -ForegroundColor Green } catch { $message = Remove-SensitiveData -Text $_.Exception.Message Write-InstallLog "Copying bot.py failed: $message" -LogPath $LogPath -Level ERROR Write-Host " FAILED: $message" -ForegroundColor Red return @{ Success = $false; Error = "Could not copy bot.py" } } # ---- 4: configuration ----------------------------------------------- Write-Host " [4/4] Configuration" -ForegroundColor Gray try { New-BotConfigFile -ConfigPath $configPath ` -Prerequisites $Prerequisites -DiscordConfig $DiscordConfig | Out-Null Write-InstallLog "config.json written to $configPath" -LogPath $LogPath Write-Host " OK config.json (readable only by you)" -ForegroundColor Green } catch { $message = Remove-SensitiveData -Text $_.Exception.Message Write-InstallLog "Writing config.json failed: $message" -LogPath $LogPath -Level ERROR Write-Host " FAILED: $message" -ForegroundColor Red return @{ Success = $false; Error = "Could not write config.json" } } # ---- verification --------------------------------------------------- # Verify what was produced, not what was intended: this is the check that # would have caught the config schema mismatch years ago. $verification = Test-InstallationResult -InstallPath $InstallPath -ConfigPath $configPath if (-not $verification.Valid) { foreach ($problem in $verification.Problems) { Write-InstallLog "Verification failed: $problem" -LogPath $LogPath -Level ERROR Write-Host " $problem" -ForegroundColor Red } return @{ Success = $false; Error = "Verification of the installed files failed" } } Write-InstallLog "Verification passed" -LogPath $LogPath return @{ Success = $true InstallPath = $InstallPath ConfigPath = $configPath LogPath = $LogPath } } function Test-InstallationResult { <# .SYNOPSIS Checks that the files on disk are actually usable by the bot. .DESCRIPTION Re-reads config.json and confirms every key bot.py declares in REQUIRED_KEYS is present and non-empty, reading the list from the installed bot.py so the two cannot drift apart. #> [OutputType([hashtable])] param( [Parameter(Mandatory = $true)][string]$InstallPath, [Parameter(Mandatory = $true)][string]$ConfigPath ) $problems = @() $botPath = Join-Path $InstallPath "bot.py" if (-not (Test-Path $botPath)) { $problems += "bot.py is missing from $InstallPath" } if (-not (Test-Path $ConfigPath)) { $problems += "config.json is missing from $InstallPath" } if ($problems.Count -gt 0) { return @{ Valid = $false; Problems = $problems } } $config = $null try { $config = Get-Content $ConfigPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { return @{ Valid = $false; Problems = @("config.json is not valid JSON: $($_.Exception.Message)") } } $requiredKeys = @('bot_token', 'guild_id', 'voice_channel_id', 'audio_device_name') try { $botSource = Get-Content $botPath -Raw -ErrorAction Stop if ($botSource -match 'REQUIRED_KEYS\s*=\s*\[(.*?)\]') { $parsed = [regex]::Matches($Matches[1], '"([^"]+)"') | ForEach-Object { $_.Groups[1].Value } if ($parsed) { $requiredKeys = $parsed } } } catch { # Fall back to the hardcoded list above. } foreach ($key in $requiredKeys) { if (-not ($config.PSObject.Properties.Name -contains $key)) { $problems += "config.json is missing the key '$key' that bot.py requires" } elseif ([string]::IsNullOrWhiteSpace([string]$config.$key)) { $problems += "config.json has an empty value for '$key', which bot.py rejects" } } return @{ Valid = ($problems.Count -eq 0); Problems = $problems } } function Get-RequirementsPath { <# .SYNOPSIS Locates requirements.txt for both PSGallery and repository layouts. #> [OutputType([string])] param() $candidates = @() try { $moduleBase = (Get-Module BATCRelayBot).ModuleBase if ($moduleBase) { $candidates += Join-Path $moduleBase "requirements.txt" $candidates += Join-Path (Split-Path -Parent $moduleBase) "requirements.txt" } } catch {} $candidates += "requirements.txt" $candidates += (Join-Path ".." "requirements.txt") foreach ($candidate in $candidates) { if ($candidate -and (Test-Path $candidate)) { return (Resolve-Path $candidate).Path } } return $null } function Get-BotFilesPath { <# .SYNOPSIS Locates the directory containing bot.py. #> [OutputType([string])] param() $candidates = @() try { $moduleBase = (Get-Module BATCRelayBot).ModuleBase if ($moduleBase) { $candidates += $moduleBase $candidates += (Split-Path -Parent $moduleBase) } } catch {} $candidates += "." $candidates += ".." foreach ($candidate in $candidates) { if ($candidate -and (Test-Path (Join-Path $candidate "bot.py"))) { return (Resolve-Path $candidate).Path } } return $null } Export-ModuleMember -Function @( 'Start-Installation', 'Test-InstallationResult', 'Get-RequirementsPath', 'Get-BotFilesPath' ) |