-
-
Save acepace/d71a71c9cec4bed1ec1da0a6bd7b9a12 to your computer and use it in GitHub Desktop.
Boxstarter Commands for a new Windows box.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -RunAsAdministrator | |
| #Requires -Version 5.1 | |
| <# | |
| .SYNOPSIS | |
| Personal Windows 11 setup script for a new PC. | |
| .DESCRIPTION | |
| Single-file, Gist-friendly bootstrap script. | |
| Order of operations: | |
| 1. Bootstrap | |
| 2. OS configuration | |
| 3. OS installations | |
| 4. Program installations | |
| 5. Program and user configuration | |
| .NOTES | |
| Recommended Gist invocation from an elevated Windows PowerShell prompt: | |
| Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force | |
| $setup = Join-Path $env:TEMP 'setup.ps1' | |
| irm https://gist.githubusercontent.com/<user>/<gist-id>/raw/setup.ps1 -OutFile $setup | |
| & $setup | |
| Do not use `irm ... | iex` for this script. The Windows PowerShell 5.1 bootstrap | |
| relaunches the script under PowerShell 7 and therefore needs a real .ps1 path. | |
| #> | |
| [CmdletBinding()] | |
| param() | |
| $script:StartedInPowerShellEdition = $PSVersionTable.PSEdition | |
| $script:StartedInPowerShellVersion = $PSVersionTable.PSVersion.ToString() | |
| function Invoke-NativeBootstrap { | |
| param( | |
| [Parameter(Mandatory)][string]$FilePath, | |
| [Parameter(Mandatory)][AllowEmptyString()][string[]]$Arguments | |
| ) | |
| Write-Host ">>> $FilePath $($Arguments -join ' ')" | |
| & $FilePath @Arguments | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "$FilePath failed with exit code $LASTEXITCODE" | |
| } | |
| } | |
| # Windows PowerShell 5.1 is only a launcher. The real script runs under PowerShell 7+. | |
| if ($PSVersionTable.PSVersion.Major -lt 7) { | |
| Write-Host 'Windows PowerShell detected. Bootstrapping PowerShell 7 and relaunching...' | |
| if (-not $PSCommandPath) { | |
| throw 'This script must be run from a .ps1 file path, not via `irm ... | iex`, so it can relaunch under PowerShell 7.' | |
| } | |
| if (-not (Get-Command -Name winget -ErrorAction SilentlyContinue)) { | |
| throw 'winget was not found. Install or update App Installer from Microsoft Store, then rerun.' | |
| } | |
| $pwshCommand = Get-Command -Name pwsh -ErrorAction SilentlyContinue | |
| $pwshPath = $null | |
| if ($pwshCommand) { | |
| $pwshPath = $pwshCommand.Source | |
| } | |
| else { | |
| $defaultPwshPath = Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe' | |
| if (Test-Path -Path $defaultPwshPath) { | |
| $pwshPath = $defaultPwshPath | |
| } | |
| } | |
| if (-not $pwshPath) { | |
| Invoke-NativeBootstrap -FilePath 'winget' -Arguments @( | |
| 'install', | |
| '--id', 'Microsoft.PowerShell', | |
| '-e', | |
| '--source', 'winget', | |
| '--accept-package-agreements', | |
| '--accept-source-agreements' | |
| ) | |
| $defaultPwshPath = Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe' | |
| if (Test-Path -Path $defaultPwshPath) { | |
| $pwshPath = $defaultPwshPath | |
| } | |
| else { | |
| $pwshCommand = Get-Command -Name pwsh -ErrorAction SilentlyContinue | |
| if ($pwshCommand) { | |
| $pwshPath = $pwshCommand.Source | |
| } | |
| } | |
| } | |
| if (-not $pwshPath) { | |
| throw 'PowerShell 7 was installed, but pwsh.exe could not be found. Open a new elevated shell and rerun this script.' | |
| } | |
| Write-Host "Relaunching under PowerShell 7: $pwshPath" | |
| & $pwshPath -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath | |
| exit $LASTEXITCODE | |
| } | |
| if ($PSVersionTable.PSVersion.Major -lt 7) { | |
| throw 'PowerShell 7 or newer is required after bootstrap.' | |
| } | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| $ProgressPreference = 'SilentlyContinue' | |
| $script:NeedsReboot = $false | |
| $script:DismFeatureStates = $null | |
| $script:DismCapabilityStates = $null | |
| $script:RunStamp = Get-Date -Format yyyyMMdd-HHmmss | |
| $script:LogPath = Join-Path $env:TEMP "acepace-pc-setup-$script:RunStamp.log" | |
| $script:TranscriptStarted = $false | |
| function Write-Log { | |
| param( | |
| [Parameter(Mandatory)][string]$Message, | |
| [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')][string]$Level = 'INFO' | |
| ) | |
| $line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message | |
| switch ($Level) { | |
| 'WARN' { Write-Warning $line } | |
| 'ERROR' { Write-Host $line -ForegroundColor Red } | |
| 'DEBUG' { Write-Host $line -ForegroundColor DarkGray } | |
| default { Write-Host $line } | |
| } | |
| } | |
| function Write-Heading { | |
| param([Parameter(Mandatory)][string]$Text) | |
| Write-Host '' | |
| Write-Host "==== $Text ====" -ForegroundColor Cyan | |
| Write-Log "==== $Text ====" -Level DEBUG | |
| } | |
| function Write-Step { | |
| param([Parameter(Mandatory)][string]$Text) | |
| Write-Host "-- $Text" -ForegroundColor DarkCyan | |
| Write-Log "-- $Text" -Level DEBUG | |
| } | |
| function Invoke-Phase { | |
| param( | |
| [Parameter(Mandatory)][string]$Name, | |
| [Parameter(Mandatory)][scriptblock]$ScriptBlock | |
| ) | |
| Write-Heading $Name | |
| Write-Log "PHASE START: $Name" -Level DEBUG | |
| try { | |
| & $ScriptBlock | |
| Write-Log "PHASE END: $Name" -Level DEBUG | |
| } | |
| catch { | |
| Write-Log "PHASE FAILED: $Name" -Level ERROR | |
| Write-Log $_.Exception.ToString() -Level ERROR | |
| throw | |
| } | |
| } | |
| function Invoke-Native { | |
| param( | |
| [Parameter(Mandatory)][string]$FilePath, | |
| [Parameter(Mandatory)][AllowEmptyString()][string[]]$Arguments | |
| ) | |
| Write-Log ">>> $FilePath $($Arguments -join ' ')" -Level DEBUG | |
| & $FilePath @Arguments | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "$FilePath failed with exit code $LASTEXITCODE" | |
| } | |
| } | |
| function Invoke-WindowsPowerShellScript { | |
| param([Parameter(Mandatory)][string]$ScriptText) | |
| $scriptPath = Join-Path $env:TEMP "acepace-windows-powershell-$script:RunStamp.ps1" | |
| try { | |
| Set-Content -Path $scriptPath -Value $ScriptText -Encoding UTF8 | |
| Invoke-Native -FilePath 'powershell.exe' -Arguments @( | |
| '-NoProfile', | |
| '-ExecutionPolicy', 'Bypass', | |
| '-File', $scriptPath | |
| ) | |
| } | |
| finally { | |
| Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| function Test-CommandExists { | |
| param([Parameter(Mandatory)][string]$Name) | |
| return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue) | |
| } | |
| function Ensure-RegistryKey { | |
| param([Parameter(Mandatory)][string]$Path) | |
| if (-not (Test-Path -Path $Path)) { | |
| New-Item -Path $Path -Force | Out-Null | |
| } | |
| } | |
| function Set-Dword { | |
| param( | |
| [Parameter(Mandatory)][string]$Path, | |
| [Parameter(Mandatory)][string]$Name, | |
| [Parameter(Mandatory)][int]$Value, | |
| [switch]$Required | |
| ) | |
| Write-Log "Registry desired state: $Path $Name=$Value" -Level DEBUG | |
| try { | |
| Ensure-RegistryKey -Path $Path | |
| $existing = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue | |
| if ($existing -and $existing.$Name -eq $Value) { | |
| Write-Log "Registry already set: $Path $Name=$Value" -Level DEBUG | |
| return | |
| } | |
| Set-ItemProperty -Path $Path -Name $Name -Type DWord -Value $Value -ErrorAction Stop | |
| Write-Log "Registry updated: $Path $Name=$Value" -Level DEBUG | |
| } | |
| catch { | |
| $message = "Registry update failed: $Path $Name=$Value :: $($_.Exception.GetType().FullName): $($_.Exception.Message)" | |
| if ($Required) { | |
| Write-Log $message -Level ERROR | |
| throw | |
| } | |
| Write-Log "$message; continuing because this is a preference setting." -Level WARN | |
| } | |
| } | |
| function Test-Windows11OrLater { | |
| $os = Get-CimInstance -ClassName Win32_OperatingSystem | |
| $version = [version]$os.Version | |
| return ($version.Major -gt 10) -or (($version.Major -eq 10) -and ($version.Build -ge 22000)) | |
| } | |
| function Ensure-PSGalleryAccess { | |
| Write-Step 'Prepare PowerShell Gallery access' | |
| if (-not (Test-CommandExists Get-PSResourceRepository)) { | |
| throw 'PSResourceGet is not available. Install or update PowerShell 7.4+ and rerun the script.' | |
| } | |
| $repo = Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue | |
| if ($repo) { | |
| Set-PSResourceRepository -Name PSGallery -Trusted -ErrorAction Stop | |
| } | |
| else { | |
| Register-PSResourceRepository -PSGallery -Trusted -ErrorAction Stop | |
| } | |
| } | |
| function Ensure-PowerShellModule { | |
| param([Parameter(Mandatory)][string]$Name) | |
| if (-not (Test-CommandExists Install-PSResource)) { | |
| throw 'Install-PSResource is not available. Install or update PowerShell 7.4+ and rerun the script.' | |
| } | |
| Write-Log "Installing or updating PowerShell resource for all users: $Name" -Level DEBUG | |
| Install-PSResource -Name $Name -Repository PSGallery -Scope AllUsers -TrustRepository -AcceptLicense -Quiet -ErrorAction Stop | |
| } | |
| function Set-GitConfigIfDifferent { | |
| param( | |
| [Parameter(Mandatory)][string]$Name, | |
| [Parameter(Mandatory)][string]$Value | |
| ) | |
| if (-not (Test-CommandExists git)) { | |
| throw 'git is not available after program installation.' | |
| } | |
| $current = & git config --global --get $Name 2>$null | |
| if ($LASTEXITCODE -eq 0 -and $current -eq $Value) { | |
| Write-Log "Git config already set: $Name=$Value" -Level DEBUG | |
| return | |
| } | |
| & git config --global $Name $Value | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "git config failed for $Name" | |
| } | |
| } | |
| function Invoke-DismCapture { | |
| param( | |
| [Parameter(Mandatory)][string[]]$Arguments | |
| ) | |
| Write-Log "❯ dism.exe $($Arguments -join ' ')" -Level DEBUG | |
| $output = & dism.exe @Arguments 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| foreach ($line in $output) { | |
| Write-Log "DISM: $line" -Level DEBUG | |
| } | |
| return [pscustomobject]@{ | |
| ExitCode = $exitCode | |
| Output = @($output | ForEach-Object { [string]$_ }) | |
| Text = ($output | ForEach-Object { [string]$_ }) -join "`n" | |
| } | |
| } | |
| function Invoke-DismRequired { | |
| param( | |
| [Parameter(Mandatory)][string[]]$Arguments | |
| ) | |
| $result = Invoke-DismCapture -Arguments $Arguments | |
| switch ($result.ExitCode) { | |
| 0 { | |
| return $result | |
| } | |
| 3010 { | |
| Write-Log "dism.exe completed successfully and requested a reboot: $($Arguments -join ' ')" -Level WARN | |
| $script:NeedsReboot = $true | |
| return $result | |
| } | |
| default { | |
| throw "dism.exe $($Arguments -join ' ') failed with exit code $($result.ExitCode). Output:`n$($result.Text)" | |
| } | |
| } | |
| } | |
| function Get-DismFeatureStates { | |
| if ($null -ne $script:DismFeatureStates) { | |
| return $script:DismFeatureStates | |
| } | |
| Write-Log 'Enumerating Windows optional features with dism.exe' -Level DEBUG | |
| $result = Invoke-DismCapture -Arguments @('/Online', '/Get-Features') | |
| if ($result.ExitCode -ne 0) { | |
| throw "Could not enumerate optional features with dism.exe. Exit code: $($result.ExitCode). Output:`n$($result.Text)" | |
| } | |
| $states = @{} | |
| $currentName = $null | |
| foreach ($line in $result.Output) { | |
| if ($line -match '^\s*Feature Name\s*:\s*(?<name>.+?)\s*$') { | |
| $currentName = $Matches.name.Trim() | |
| continue | |
| } | |
| if ($null -ne $currentName -and $line -match '^\s*State\s*:\s*(?<state>.+?)\s*$') { | |
| $states[$currentName] = $Matches.state.Trim() | |
| $currentName = $null | |
| } | |
| } | |
| Write-Log "Discovered $($states.Count) Windows optional features." -Level DEBUG | |
| $script:DismFeatureStates = $states | |
| return $script:DismFeatureStates | |
| } | |
| function Set-OptionalFeatureState { | |
| param( | |
| [Parameter(Mandatory)][string]$FeatureName, | |
| [Parameter(Mandatory)][ValidateSet('Enable', 'Disable')][string]$State | |
| ) | |
| $states = Get-DismFeatureStates | |
| if (-not $states.ContainsKey($FeatureName)) { | |
| Write-Log "Optional feature is not available on this Windows build; skipping: $FeatureName" -Level WARN | |
| return | |
| } | |
| $currentState = [string]$states[$FeatureName] | |
| if ($State -eq 'Enable' -and $currentState -match '^Enabled|^Enable Pending') { | |
| Write-Log "Already enabled or pending enable: $FeatureName ($currentState)" -Level DEBUG | |
| return | |
| } | |
| if ($State -eq 'Disable' -and $currentState -match '^Disabled|^Disable Pending') { | |
| Write-Log "Already disabled or pending disable: $FeatureName ($currentState)" -Level DEBUG | |
| return | |
| } | |
| if ($State -eq 'Enable') { | |
| Invoke-DismRequired -Arguments @('/Online', '/NoRestart', '/Enable-Feature', "/FeatureName:$FeatureName", '/All') | Out-Null | |
| } | |
| else { | |
| Invoke-DismRequired -Arguments @('/Online', '/NoRestart', '/Disable-Feature', "/FeatureName:$FeatureName") | Out-Null | |
| } | |
| # Keep the cached state coherent for the rest of this run. | |
| if ($State -eq 'Enable') { | |
| $states[$FeatureName] = 'Enable Pending' | |
| } | |
| else { | |
| $states[$FeatureName] = 'Disable Pending' | |
| } | |
| $script:NeedsReboot = $true | |
| } | |
| function Get-DismCapabilityStates { | |
| if ($null -ne $script:DismCapabilityStates) { | |
| return $script:DismCapabilityStates | |
| } | |
| Write-Log 'Enumerating Windows capabilities with dism.exe' -Level DEBUG | |
| $result = Invoke-DismCapture -Arguments @('/Online', '/Get-Capabilities') | |
| if ($result.ExitCode -ne 0) { | |
| throw "Could not enumerate Windows capabilities with dism.exe. Exit code: $($result.ExitCode). Output:`n$($result.Text)" | |
| } | |
| $states = @{} | |
| $currentName = $null | |
| foreach ($line in $result.Output) { | |
| if ($line -match '^\s*Capability (?:Identity|Name)\s*:\s*(?<name>.+?)\s*$') { | |
| $currentName = $Matches.name.Trim() | |
| continue | |
| } | |
| if ($null -ne $currentName -and $line -match '^\s*State\s*:\s*(?<state>.+?)\s*$') { | |
| $states[$currentName] = $Matches.state.Trim() | |
| $currentName = $null | |
| } | |
| } | |
| Write-Log "Discovered $($states.Count) Windows capabilities." -Level DEBUG | |
| $script:DismCapabilityStates = $states | |
| return $script:DismCapabilityStates | |
| } | |
| function Set-WindowsCapabilityState { | |
| param( | |
| [Parameter(Mandatory)][string]$CapabilityName, | |
| [Parameter(Mandatory)][ValidateSet('Install', 'Remove')][string]$State | |
| ) | |
| $states = Get-DismCapabilityStates | |
| if (-not $states.ContainsKey($CapabilityName)) { | |
| Write-Log "Windows capability is not available on this build; skipping: $CapabilityName" -Level WARN | |
| return | |
| } | |
| $currentState = [string]$states[$CapabilityName] | |
| if ($State -eq 'Install' -and $currentState -match '^(Installed|Install Pending)$') { | |
| Write-Log "Already installed or pending install: $CapabilityName ($currentState)" -Level DEBUG | |
| return | |
| } | |
| if ($State -eq 'Remove' -and $currentState -match '^(Not Present|Uninstall Pending)$') { | |
| Write-Log "Already absent or pending removal: $CapabilityName ($currentState)" -Level DEBUG | |
| if ($currentState -eq 'Uninstall Pending') { | |
| $script:NeedsReboot = $true | |
| } | |
| return | |
| } | |
| if ($State -eq 'Install') { | |
| $result = Invoke-DismCapture -Arguments @('/Online', '/NoRestart', '/Add-Capability', "/CapabilityName:$CapabilityName") | |
| } | |
| else { | |
| $result = Invoke-DismCapture -Arguments @('/Online', '/NoRestart', '/Remove-Capability', "/CapabilityName:$CapabilityName") | |
| } | |
| switch ($result.ExitCode) { | |
| 0 { | |
| if ($State -eq 'Install') { | |
| $states[$CapabilityName] = 'Install Pending' | |
| } | |
| else { | |
| $states[$CapabilityName] = 'Uninstall Pending' | |
| } | |
| $script:NeedsReboot = $true | |
| return | |
| } | |
| 3010 { | |
| Write-Log "dism.exe completed successfully and requested a reboot for capability $CapabilityName" -Level WARN | |
| if ($State -eq 'Install') { | |
| $states[$CapabilityName] = 'Install Pending' | |
| } | |
| else { | |
| $states[$CapabilityName] = 'Uninstall Pending' | |
| } | |
| $script:NeedsReboot = $true | |
| return | |
| } | |
| default { | |
| throw "dism.exe capability operation failed for $CapabilityName with exit code $($result.ExitCode). Output:`n$($result.Text)" | |
| } | |
| } | |
| } | |
| function Remove-WindowsCapabilityIfPresent { | |
| param([Parameter(Mandatory)][string]$CapabilityName) | |
| Set-WindowsCapabilityState -CapabilityName $CapabilityName -State Remove | |
| } | |
| function Add-WindowsCapabilityIfMissing { | |
| param([Parameter(Mandatory)][string]$CapabilityName) | |
| Set-WindowsCapabilityState -CapabilityName $CapabilityName -State Install | |
| } | |
| function Remove-AppxByName { | |
| param([Parameter(Mandatory)][string[]]$Names) | |
| $namesJson = $Names | ConvertTo-Json -Compress | |
| $appxScript = @" | |
| `$ErrorActionPreference = 'Stop' | |
| `$names = @' | |
| $namesJson | |
| '@ | ConvertFrom-Json | |
| foreach (`$name in `$names) { | |
| Write-Host "-- Remove bundled app if present: `$name" | |
| try { | |
| `$packages = Get-AppxPackage -Name `$name -AllUsers -ErrorAction Stop | |
| if (-not `$packages) { | |
| Write-Host "Installed Appx package not found: `$name" | |
| continue | |
| } | |
| `$packages | Remove-AppxPackage -AllUsers -ErrorAction Stop | |
| } | |
| catch { | |
| Write-Warning "Could not remove installed Appx package '`$name'; continuing. `$(`$_.Exception.GetType().FullName): `$(`$_.Exception.Message)" | |
| } | |
| } | |
| "@ | |
| Invoke-WindowsPowerShellScript -ScriptText $appxScript | |
| } | |
| function Restart-ExplorerShell { | |
| Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue | |
| } | |
| function Add-OrUpdateTerminalProfile { | |
| param( | |
| [Parameter(Mandatory)][hashtable]$Settings, | |
| [Parameter(Mandatory)][string]$Guid, | |
| [Parameter(Mandatory)][string]$Name, | |
| [Parameter(Mandatory)][bool]$Hidden | |
| ) | |
| $list = @($Settings['profiles']['list']) | |
| $existing = $list | Where-Object { $_['guid'] -eq $Guid } | Select-Object -First 1 | |
| if ($existing) { | |
| $existing['name'] = $Name | |
| $existing['hidden'] = $Hidden | |
| } | |
| else { | |
| $Settings['profiles']['list'] = $list + @{ | |
| guid = $Guid | |
| name = $Name | |
| hidden = $Hidden | |
| } | |
| } | |
| } | |
| function Initialize-Bootstrap { | |
| Start-Transcript -Path $script:LogPath | Out-Null | |
| $script:TranscriptStarted = $true | |
| Write-Step 'Preflight checks' | |
| if (-not (Test-Windows11OrLater)) { | |
| throw 'This script expects Windows 11 or later.' | |
| } | |
| if (-not (Test-CommandExists winget)) { | |
| throw 'winget was not found. Install or update App Installer from Microsoft Store, then rerun.' | |
| } | |
| Write-Host "Computer: $env:COMPUTERNAME" | |
| Write-Host "User: $env:USERNAME" | |
| Write-Host "Started in PowerShell: $script:StartedInPowerShellEdition $script:StartedInPowerShellVersion" | |
| Write-Host "Running in PowerShell: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)" | |
| Write-Host "Log file: $script:LogPath" | |
| Write-Step 'Use process-scoped execution policy bypass for this run' | |
| Set-ExecutionPolicy Bypass -Scope Process -Force | |
| Write-Step 'Update WinGet sources' | |
| Invoke-Native -FilePath 'winget' -Arguments @('source', 'update') | |
| } | |
| function Set-OsConfiguration { | |
| Write-Step 'Documented policy-backed Windows settings' | |
| Set-Dword -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 1 -Required | |
| Set-Dword -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name DisableWindowsConsumerFeatures -Value 1 | |
| Set-Dword -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' -Name NoLockScreen -Value 1 | |
| Set-Dword -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search' -Name AllowCortana -Value 0 | |
| Set-Dword -Path 'HKCU:\Software\Policies\Microsoft\Windows\Explorer' -Name HideTaskViewButton -Value 1 | |
| Write-Step 'Explorer preferences; best-effort user settings' | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name Hidden -Value 1 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name ShowSuperHidden -Value 1 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name HideFileExt -Value 0 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name LaunchTo -Value 1 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name AutoCheckSelect -Value 1 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer' -Name ShowRecent -Value 0 | |
| Set-Dword -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer' -Name ShowFrequent -Value 0 | |
| Write-Step 'Remove installed bundled apps' | |
| $appsToRemove = @( | |
| 'king.com.CandyCrushSaga', | |
| 'Microsoft.BingWeather', | |
| 'Microsoft.BingNews', | |
| 'Microsoft.BingSports', | |
| 'Microsoft.BingFinance', | |
| 'Microsoft.3DBuilder', | |
| 'Microsoft.WindowsAlarms', | |
| 'Microsoft.XboxApp', | |
| 'Microsoft.MicrosoftSolitaireCollection', | |
| 'Microsoft.People', | |
| 'Microsoft.MicrosoftJournal', | |
| 'Microsoft.ZuneMusic', | |
| 'Microsoft.Getstarted', | |
| 'Microsoft.GetHelp', | |
| 'Microsoft.ZuneVideo', | |
| 'Microsoft.WindowsSoundRecorder', | |
| 'Microsoft.WindowsMaps', | |
| '*Autodesk*', | |
| '*BubbleWitch*', | |
| '*CandyCrush*', | |
| '*Dropbox*', | |
| '*Facebook*', | |
| '*Keeper*', | |
| '*MarchofEmpires*', | |
| '*Minecraft*', | |
| '*Netflix*', | |
| '*Plex*', | |
| '*Spotify*', | |
| '*Twitter*', | |
| '*TikTok*', | |
| 'Microsoft.MicrosoftStickyNotes', | |
| 'Microsoft.Todos', | |
| 'Microsoft.SkypeApp' | |
| ) | |
| Remove-AppxByName -Names $appsToRemove | |
| } | |
| function Install-OsComponents { | |
| Write-Step 'Disable legacy Windows optional features' | |
| $featuresToDisable = @( | |
| 'Printing-Foundation-InternetPrinting-Client', | |
| 'Printing-XPSServices-Features', | |
| 'WorkFolders-Client', | |
| 'DirectPlay', | |
| 'LegacyComponents', | |
| 'SimpleTCP', | |
| 'TelnetClient', | |
| 'TFTP', | |
| 'TIFFIFilter', | |
| 'SMB1Protocol', | |
| 'SMB1Protocol-Client', | |
| 'SMB1Protocol-Server', | |
| 'SMB1Protocol-Deprecation', | |
| 'WindowsMediaPlayer' | |
| ) | |
| foreach ($feature in $featuresToDisable) { | |
| Set-OptionalFeatureState -FeatureName $feature -State Disable | |
| } | |
| Write-Step 'Enable virtualization, containers, and developer features' | |
| $featuresToEnable = @( | |
| 'Microsoft-Windows-Subsystem-Linux', | |
| 'VirtualMachinePlatform', | |
| 'HypervisorPlatform', | |
| 'Microsoft-Hyper-V-All', | |
| 'Containers', | |
| 'Containers-DisposableClientVM', | |
| 'Client-ProjFS' | |
| ) | |
| foreach ($feature in $featuresToEnable) { | |
| Set-OptionalFeatureState -FeatureName $feature -State Enable | |
| } | |
| Write-Step 'Remove optional Windows capabilities' | |
| $capabilitiesToRemove = @( | |
| 'App.StepsRecorder~~~~0.0.1.0', | |
| 'Browser.InternetExplorer~~~~0.0.11.0', | |
| 'Media.WindowsMediaPlayer~~~~0.0.12.0', | |
| 'Microsoft.Windows.PowerShell.ISE~~~~0.0.1.0', | |
| 'MathRecognizer~~~~0.0.1.0' | |
| ) | |
| foreach ($capability in $capabilitiesToRemove) { | |
| Remove-WindowsCapabilityIfPresent -CapabilityName $capability | |
| } | |
| Write-Step 'Install useful Windows capabilities' | |
| $capabilitiesToInstall = @( | |
| 'OpenSSH.Client~~~~0.0.1.0', | |
| 'Tools.DeveloperMode.Core~~~~0.0.1.0', | |
| 'Tools.Graphics.DirectX~~~~0.0.1.0' | |
| ) | |
| foreach ($capability in $capabilitiesToInstall) { | |
| Add-WindowsCapabilityIfMissing -CapabilityName $capability | |
| } | |
| Write-Step 'Install WSL' | |
| $wslRequiredFeatures = @( | |
| 'Microsoft-Windows-Subsystem-Linux', | |
| 'VirtualMachinePlatform' | |
| ) | |
| $featureStates = Get-DismFeatureStates | |
| $wslBlockedByPendingFeature = $false | |
| foreach ($featureName in $wslRequiredFeatures) { | |
| if (-not $featureStates.ContainsKey($featureName)) { | |
| Write-Log "WSL prerequisite feature is not available on this Windows build: $featureName" -Level WARN | |
| $wslBlockedByPendingFeature = $true | |
| continue | |
| } | |
| $featureState = [string]$featureStates[$featureName] | |
| if ($featureState -ne 'Enabled') { | |
| Write-Log "WSL prerequisite feature is not active yet: $featureName ($featureState). Reboot, then rerun the script to complete WSL installation." -Level WARN | |
| $wslBlockedByPendingFeature = $true | |
| } | |
| } | |
| if ($wslBlockedByPendingFeature) { | |
| $script:NeedsReboot = $true | |
| Write-Log 'Skipping WSL installation for this run because WSL prerequisites are missing or pending reboot.' -Level WARN | |
| } | |
| else { | |
| Invoke-Native -FilePath 'wsl' -Arguments @('--update', '--web-download') | |
| Invoke-Native -FilePath 'wsl' -Arguments @('--set-default-version', '2') | |
| $installedDistributions = @(& wsl --list --quiet 2>$null | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "wsl --list --quiet failed with exit code $LASTEXITCODE" | |
| } | |
| if ($installedDistributions -contains 'Ubuntu') { | |
| Write-Log 'WSL Ubuntu distribution already installed; skipping distro install.' -Level INFO | |
| } | |
| elseif ($installedDistributions.Count -gt 0) { | |
| Write-Log "WSL already has installed distributions: $($installedDistributions -join ', '). Skipping default Ubuntu install." -Level INFO | |
| } | |
| else { | |
| Invoke-Native -FilePath 'wsl' -Arguments @('--install', '--distribution', 'Ubuntu', '--no-launch') | |
| } | |
| } | |
| } | |
| function Install-Programs { | |
| Write-Step 'Create temporary WinGet Configuration file' | |
| $wingetConfigPath = Join-Path $env:TEMP 'acepace.winget' | |
| $packages = @( | |
| @{ ResourceId = 'windowsTerminal'; PackageId = 'Microsoft.WindowsTerminal'; Description = 'Install Windows Terminal' }, | |
| @{ ResourceId = 'git'; PackageId = 'Git.Git'; Description = 'Install Git' }, | |
| @{ ResourceId = 'githubCli'; PackageId = 'GitHub.cli'; Description = 'Install GitHub CLI' }, | |
| @{ ResourceId = 'powertoys'; PackageId = 'Microsoft.PowerToys'; Description = 'Install PowerToys' }, | |
| @{ ResourceId = 'docker'; PackageId = 'Docker.DockerDesktop'; Description = 'Install Docker Desktop' }, | |
| @{ ResourceId = 'wireshark'; PackageId = 'WiresharkFoundation.Wireshark'; Description = 'Install Wireshark' }, | |
| @{ ResourceId = 'nanazip'; PackageId = 'M2Team.NanaZip'; Description = 'Install NanaZip' }, | |
| @{ ResourceId = 'notepadplusplus'; PackageId = 'Notepad++.Notepad++'; Description = 'Install Notepad++' }, | |
| @{ ResourceId = 'vlc'; PackageId = 'VideoLAN.VLC'; Description = 'Install VLC' }, | |
| @{ ResourceId = 'paintdotnet'; PackageId = 'dotPDN.PaintDotNet'; Description = 'Install Paint.NET' }, | |
| @{ ResourceId = 'vivaldi'; PackageId = 'Vivaldi.Vivaldi'; Description = 'Install Vivaldi' }, | |
| @{ ResourceId = 'fnm'; PackageId = 'Schniz.fnm'; Description = 'Install fnm' }, | |
| @{ ResourceId = 'ohmyposh'; PackageId = 'JanDeDobbeleer.OhMyPosh'; Description = 'Install Oh My Posh' }, | |
| @{ ResourceId = 'jetbrainsMonoNerdFont'; PackageId = 'DEVCOM.JetBrainsMonoNerdFont'; Description = 'Install JetBrains Mono Nerd Font' }, | |
| @{ ResourceId = 'procexp'; PackageId = 'Microsoft.Sysinternals.ProcessExplorer'; Description = 'Install Sysinternals Process Explorer' }, | |
| @{ ResourceId = 'autoruns'; PackageId = 'Microsoft.Sysinternals.Autoruns'; Description = 'Install Sysinternals Autoruns' }, | |
| @{ ResourceId = 'procmon'; PackageId = 'Microsoft.Sysinternals.ProcessMonitor'; Description = 'Install Sysinternals Process Monitor' }, | |
| @{ ResourceId = 'strings'; PackageId = 'Microsoft.Sysinternals.Strings'; Description = 'Install Sysinternals Strings' }, | |
| @{ ResourceId = 'zoomit'; PackageId = 'Microsoft.Sysinternals.ZoomIt'; Description = 'Install Sysinternals ZoomIt' }, | |
| @{ ResourceId = 'sysmon'; PackageId = 'Microsoft.Sysinternals.Sysmon'; Description = 'Install Sysinternals Sysmon' }, | |
| @{ ResourceId = 'vcredist2010'; PackageId = 'Microsoft.VCRedist.2010.x64'; Description = 'Install Visual C++ Redistributable 2010 x64' }, | |
| @{ ResourceId = 'vcredist2012'; PackageId = 'Microsoft.VCRedist.2012.x64'; Description = 'Install Visual C++ Redistributable 2012 x64' }, | |
| @{ ResourceId = 'vcredist2013'; PackageId = 'Microsoft.VCRedist.2013.x64'; Description = 'Install Visual C++ Redistributable 2013 x64' }, | |
| @{ ResourceId = 'vcredist2015plus'; PackageId = 'Microsoft.VCRedist.2015+.x64'; Description = 'Install Visual C++ Redistributable 2015+' } | |
| ) | |
| $lines = @( | |
| '# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2', | |
| 'properties:', | |
| ' assertions:', | |
| ' - resource: Microsoft.Windows.Developer/OsVersion', | |
| ' id: osVersion', | |
| ' directives:', | |
| ' description: Verify Windows 11 or newer', | |
| ' allowPrerelease: true', | |
| ' settings:', | |
| " MinVersion: '10.0.22000'", | |
| '', | |
| ' resources:', | |
| ' - resource: Microsoft.Windows.Settings/WindowsSettings', | |
| ' id: developerMode', | |
| ' directives:', | |
| ' description: Enable Developer Mode', | |
| ' allowPrerelease: true', | |
| ' securityContext: elevated', | |
| ' settings:', | |
| ' DeveloperMode: true' | |
| ) | |
| foreach ($package in $packages) { | |
| $lines += @( | |
| '', | |
| ' - resource: Microsoft.WinGet.DSC/WinGetPackage', | |
| " id: $($package.ResourceId)", | |
| ' directives:', | |
| " description: $($package.Description)", | |
| ' securityContext: elevated', | |
| ' settings:', | |
| " id: $($package.PackageId)", | |
| ' source: winget' | |
| ) | |
| } | |
| $lines += @('', ' configurationVersion: 0.2.0') | |
| Set-Content -Path $wingetConfigPath -Value ($lines -join "`r`n") -Encoding UTF8 | |
| Write-Step 'Apply WinGet Configuration' | |
| Invoke-Native -FilePath 'winget' -Arguments @('configure', '--file', $wingetConfigPath, '--accept-configuration-agreements') | |
| } | |
| function Configure-GitAndGithub { | |
| Write-Step 'Configure Git defaults' | |
| Set-GitConfigIfDifferent -Name init.defaultBranch -Value main | |
| Set-GitConfigIfDifferent -Name pull.rebase -Value false | |
| Set-GitConfigIfDifferent -Name core.autocrlf -Value false | |
| Set-GitConfigIfDifferent -Name core.longpaths -Value true | |
| if (-not (Test-CommandExists gh)) { | |
| throw 'gh is not available after program installation.' | |
| } | |
| Write-Step 'Configure Git credential helper for GitHub CLI' | |
| Set-GitConfigIfDifferent -Name credential.https://git.521000.best.helper -Value '!gh auth git-credential' | |
| Set-GitConfigIfDifferent -Name credential.https://gist.521000.best.helper -Value '!gh auth git-credential' | |
| Write-Step 'Configure SSH directory and agent' | |
| $sshDir = Join-Path $HOME '.ssh' | |
| New-Item -ItemType Directory -Path $sshDir -Force | Out-Null | |
| $acl = Get-Acl -Path $sshDir | |
| $acl.SetAccessRuleProtection($true, $false) | |
| $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($env:USERNAME, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow') | |
| $acl.SetAccessRule($rule) | |
| Set-Acl -Path $sshDir -AclObject $acl | |
| Set-Service -Name ssh-agent -StartupType Automatic | |
| Start-Service -Name ssh-agent | |
| $sshConfigPath = Join-Path $sshDir 'config' | |
| if (-not (Test-Path -Path $sshConfigPath)) { | |
| $sshConfig = @' | |
| Host github.com | |
| HostName github.com | |
| User git | |
| AddKeysToAgent yes | |
| IdentityFile ~/.ssh/id_ed25519 | |
| '@ | |
| Set-Content -Path $sshConfigPath -Value $sshConfig -Encoding ascii | |
| } | |
| $sshKeyPath = Join-Path $sshDir 'id_ed25519' | |
| if (-not (Test-Path -Path $sshKeyPath)) { | |
| Write-Step 'Generate local Ed25519 SSH key for GitHub if missing' | |
| $gitEmail = (& git config --global --get user.email 2>$null) | |
| if ([string]::IsNullOrWhiteSpace($gitEmail)) { | |
| $gitEmail = "$env:USERNAME@$env:COMPUTERNAME" | |
| } | |
| Invoke-Native -FilePath 'ssh-keygen' -Arguments @('-t', 'ed25519', '-C', $gitEmail, '-f', $sshKeyPath, '-N', '') | |
| } | |
| $publicKeyPath = "$sshKeyPath.pub" | |
| if (Test-Path -Path $publicKeyPath) { | |
| Invoke-Native -FilePath 'ssh-add' -Arguments @($sshKeyPath) | |
| } | |
| Write-Step 'Check GitHub CLI authentication' | |
| & gh auth status --hostname github.com | Out-Null | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host 'GitHub CLI is not authenticated yet. Run this after setup:' -ForegroundColor Yellow | |
| Write-Host ' gh auth login --hostname github.com --git-protocol ssh --web' -ForegroundColor Yellow | |
| } | |
| elseif (Test-Path -Path $publicKeyPath) { | |
| Write-Host 'GitHub CLI is authenticated. To upload the SSH public key if needed, run:' -ForegroundColor Yellow | |
| Write-Host " gh ssh-key add '$publicKeyPath' --title '$env:COMPUTERNAME'" -ForegroundColor Yellow | |
| } | |
| } | |
| function Configure-PowerShellProfile { | |
| Write-Step 'Install PowerShell profile modules' | |
| Ensure-PSGalleryAccess | |
| Ensure-PowerShellModule -Name PSReadLine | |
| Write-Step 'Configure non-OneDrive PowerShell module path' | |
| $safeUserModulePath = Join-Path $HOME '.powershell\Modules' | |
| New-Item -ItemType Directory -Path $safeUserModulePath -Force | Out-Null | |
| $existingUserModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'User') | |
| $userModulePaths = @($safeUserModulePath) | |
| if ($existingUserModulePath) { | |
| $userModulePaths += $existingUserModulePath -split [IO.Path]::PathSeparator | |
| } | |
| $userModulePaths = $userModulePaths | | |
| Where-Object { $_ } | | |
| Select-Object -Unique | |
| [Environment]::SetEnvironmentVariable('PSModulePath', ($userModulePaths -join [IO.Path]::PathSeparator), 'User') | |
| Write-Step 'Write local Oh My Posh config' | |
| $ompConfigDir = Join-Path $HOME '.config\oh-my-posh' | |
| $ompConfigPath = Join-Path $ompConfigDir 'acepace.omp.json' | |
| New-Item -ItemType Directory -Path $ompConfigDir -Force | Out-Null | |
| $ompConfig = @' | |
| { | |
| "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json", | |
| "version": 3, | |
| "final_space": true, | |
| "blocks": [ | |
| { | |
| "type": "prompt", | |
| "alignment": "left", | |
| "segments": [ | |
| { | |
| "type": "session", | |
| "style": "diamond", | |
| "foreground": "#ffffff", | |
| "background": "#007acc", | |
| "leading_diamond": "\ue0b6", | |
| "template": " {{ .UserName }} " | |
| }, | |
| { | |
| "type": "path", | |
| "style": "powerline", | |
| "foreground": "#ffffff", | |
| "background": "#444444", | |
| "powerline_symbol": "\ue0b0", | |
| "template": " {{ .Path }} ", | |
| "properties": { | |
| "style": "folder" | |
| } | |
| }, | |
| { | |
| "type": "git", | |
| "style": "powerline", | |
| "foreground": "#ffffff", | |
| "background": "#4e9a06", | |
| "powerline_symbol": "\ue0b0", | |
| "template": " {{ .HEAD }}{{ if .Working.Changed }} *{{ end }} ", | |
| "properties": { | |
| "fetch_status": false, | |
| "fetch_upstream_icon": false | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| '@ | |
| Set-Content -Path $ompConfigPath -Value $ompConfig -Encoding UTF8 | |
| Write-Step 'Configure PowerShell profile' | |
| New-Item -ItemType File -Path $PROFILE -Force | Out-Null | |
| $profileContent = @' | |
| $UserModulePath = Join-Path $HOME '.powershell\Modules' | |
| if (Test-Path $UserModulePath) { | |
| $ModulePaths = @($UserModulePath) + ($env:PSModulePath -split [IO.Path]::PathSeparator) | |
| $env:PSModulePath = ($ModulePaths | Where-Object { $_ } | Select-Object -Unique) -join [IO.Path]::PathSeparator | |
| } | |
| $OmpConfig = Join-Path $HOME '.config\oh-my-posh\acepace.omp.json' | |
| if ((Get-Command oh-my-posh -ErrorAction SilentlyContinue) -and (Test-Path $OmpConfig)) { | |
| oh-my-posh init pwsh --config $OmpConfig | Invoke-Expression | |
| } | |
| Set-PSReadLineOption -EditMode Windows | |
| Set-PSReadLineOption -PredictionSource History | |
| Set-PSReadLineOption -PredictionViewStyle ListView | |
| '@ | |
| $current = if (Test-Path -Path $PROFILE) { Get-Content -Path $PROFILE -Raw } else { '' } | |
| if ($current -ne $profileContent) { | |
| Set-Content -Path $PROFILE -Value $profileContent -Encoding UTF8 | |
| } | |
| } | |
| function Configure-WindowsTerminal { | |
| Write-Step 'Configure Windows Terminal' | |
| $terminalSettingsPath = Join-Path $env:LOCALAPPDATA 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json' | |
| if (-not (Test-Path -Path $terminalSettingsPath)) { | |
| New-Item -ItemType Directory -Path (Split-Path $terminalSettingsPath) -Force | Out-Null | |
| $defaultSettings = [ordered]@{ | |
| profiles = [ordered]@{ | |
| defaults = [ordered]@{} | |
| list = @() | |
| } | |
| } | |
| $defaultSettings | | |
| ConvertTo-Json -Depth 100 | | |
| Set-Content -Path $terminalSettingsPath -Encoding UTF8 | |
| } | |
| $settings = Get-Content -Path $terminalSettingsPath -Raw | ConvertFrom-Json -AsHashtable | |
| if (-not $settings.ContainsKey('profiles') -or $null -eq $settings['profiles']) { | |
| $settings['profiles'] = [ordered]@{} | |
| } | |
| if (-not $settings['profiles'].ContainsKey('defaults') -or $null -eq $settings['profiles']['defaults']) { | |
| $settings['profiles']['defaults'] = [ordered]@{} | |
| } | |
| if (-not $settings['profiles'].ContainsKey('list') -or $null -eq $settings['profiles']['list']) { | |
| $settings['profiles']['list'] = @() | |
| } | |
| # Known default profile GUIDs used by Windows Terminal. | |
| # Command Prompt: {0caa0dad-35be-5f56-a8ff-afceeeaa6101} | |
| # Windows PowerShell: {61c54bbd-c2c6-5271-96e7-009a87ff44bf} | |
| # PowerShell 7: {574e775e-4f2a-5b96-ac1e-a2962a402336} | |
| Add-OrUpdateTerminalProfile -Settings $settings -Guid '{0caa0dad-35be-5f56-a8ff-afceeeaa6101}' -Name 'Command Prompt' -Hidden $true | |
| Add-OrUpdateTerminalProfile -Settings $settings -Guid '{61c54bbd-c2c6-5271-96e7-009a87ff44bf}' -Name 'Windows PowerShell' -Hidden $true | |
| Add-OrUpdateTerminalProfile -Settings $settings -Guid '{574e775e-4f2a-5b96-ac1e-a2962a402336}' -Name 'PowerShell' -Hidden $false | |
| foreach ($profile in @($settings['profiles']['list'])) { | |
| $name = if ($profile.ContainsKey('name')) { [string]$profile['name'] } else { '' } | |
| $commandLine = if ($profile.ContainsKey('commandline')) { [string]$profile['commandline'] } else { '' } | |
| $isClassicPowerShell = | |
| $name -match '^(Windows )?PowerShell$' -or | |
| $commandLine -match 'powershell\.exe' | |
| $isCmd = | |
| $name -match '^(Command Prompt|cmd)$' -or | |
| $commandLine -match 'cmd\.exe' | |
| if ($isClassicPowerShell -or $isCmd) { | |
| $profile['hidden'] = $true | |
| } | |
| } | |
| $settings['defaultProfile'] = '{574e775e-4f2a-5b96-ac1e-a2962a402336}' | |
| $settings['profiles']['defaults']['font'] = [ordered]@{ | |
| face = 'JetBrainsMono Nerd Font' | |
| } | |
| $settings | | |
| ConvertTo-Json -Depth 100 | | |
| Set-Content -Path $terminalSettingsPath -Encoding UTF8 | |
| Write-Host "Updated Terminal settings: $terminalSettingsPath" | |
| } | |
| function Configure-ProgramsAndUser { | |
| Configure-GitAndGithub | |
| Configure-PowerShellProfile | |
| Configure-WindowsTerminal | |
| Write-Step 'Restart Explorer to apply shell settings' | |
| Restart-ExplorerShell | |
| } | |
| try { | |
| Invoke-Phase -Name '1. Bootstrap' -ScriptBlock { Initialize-Bootstrap } | |
| Invoke-Phase -Name '2. OS configuration' -ScriptBlock { Set-OsConfiguration } | |
| Invoke-Phase -Name '3. OS installations' -ScriptBlock { Install-OsComponents } | |
| Invoke-Phase -Name '4. Program installations' -ScriptBlock { Install-Programs } | |
| Invoke-Phase -Name '5. Program and user configuration' -ScriptBlock { Configure-ProgramsAndUser } | |
| Write-Heading 'Complete' | |
| Write-Host 'Setup script completed.' -ForegroundColor Green | |
| Write-Host "Log file: $script:LogPath" | |
| if ($script:NeedsReboot) { | |
| Write-Host 'A reboot is recommended because Windows features, WSL, or capabilities changed.' -ForegroundColor Yellow | |
| } | |
| } | |
| catch { | |
| Write-Log "Top-level failure: $($_.Exception.Message)" -Level ERROR | |
| Write-Host '' | |
| Write-Host 'Setup failed.' -ForegroundColor Red | |
| Write-Host "Log file: $script:LogPath" | |
| throw | |
| } | |
| finally { | |
| if ($script:TranscriptStarted) { | |
| try { | |
| Stop-Transcript | Out-Null | |
| } | |
| catch { | |
| # Transcript may already be stopped. | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment