package bootstrap import ( "context" "fmt" "strings" ) type OSPhaseResult struct { Notices []string Issues Issues Stop bool } func RunOSPhase(ctx context.Context, runner Runner) OSPhaseResult { var result OSPhaseResult if issue := runOSCommand(ctx, runner, "install Windows updates", WindowsUpdateCommand()); issue.Err != nil { result.Issues = append(result.Issues, issue) } reboot, err := DetectWindowsRebootRequired(ctx, runner) if err != nil { result.Issues = append(result.Issues, Issue{Step: "detect pending reboot", Err: err}) } else if reboot { result.Notices = append(result.Notices, "Windows Update installed changes that require a reboot. Restart Windows, then rerun this bootstrapper before package installation.") result.Stop = true return result } if issue := runOSCommand(ctx, runner, "create restore point before OS configuration", RestorePointCommand("bootstrap_windows_env: before OS configuration")); issue.Err != nil { result.Issues = append(result.Issues, issue) result.Stop = true return result } if issue := runOSCommand(ctx, runner, "run Chris Titus Tech WinUtil", WinUtilCommand()); issue.Err != nil { result.Issues = append(result.Issues, issue) } for _, step := range []struct { name string cmd CommandSpec fatal bool }{ {"apply power settings", PowerPolicyCommand(), false}, {"enable Developer Mode and Windows sudo", DeveloperModeCommand(), false}, {"apply Windows Update policy", WindowsUpdatePolicyCommand(), false}, {"apply Explorer and theme settings", ExplorerThemeCommand(), false}, {"set Vivaldi default browser associations", VivaldiDefaultBrowserCommand(), false}, {"update Microsoft Store apps", StoreUpdateCommand(), false}, {"apply taskbar pins", TaskbarCommand(), false}, {"create restore point before package installation", RestorePointCommand("bootstrap_windows_env: before package installation"), true}, } { if issue := runOSCommand(ctx, runner, step.name, step.cmd); issue.Err != nil { result.Issues = append(result.Issues, issue) if step.fatal { result.Stop = true return result } } } return result } func runOSCommand(ctx context.Context, runner Runner, step string, cmd CommandSpec) Issue { res := runner.Run(ctx, cmd.Name, cmd.Args...) if res.Err != nil { if step == "run Chris Titus Tech WinUtil" && strings.Contains(res.Err.Error(), "exit status 1") { return Issue{} } return Issue{Step: step, Err: fmt.Errorf("%w: %s", res.Err, res.CombinedOutput())} } return Issue{} } func WindowsUpdateCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' $serviceManager = New-Object -ComObject Microsoft.Update.ServiceManager try { $serviceManager.ClientApplicationID = 'bootstrap_windows_env' $serviceManager.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '') | Out-Null Write-Output 'Microsoft Update enabled for other Microsoft products.' } catch { Write-Output "Microsoft Update service registration skipped: $($_.Exception.Message)" } $session = New-Object -ComObject Microsoft.Update.Session $session.ClientApplicationID = 'bootstrap_windows_env' $searcher = $session.CreateUpdateSearcher() $search = $searcher.Search("IsInstalled=0 and IsHidden=0") $updates = New-Object -ComObject Microsoft.Update.UpdateColl for ($i = 0; $i -lt $search.Updates.Count; $i++) { $update = $search.Updates.Item($i) if ($update.InstallationBehavior.CanRequestUserInput) { Write-Output "Skipping interactive update: $($update.Title)" continue } if (-not $update.EulaAccepted) { $update.AcceptEula() } [void]$updates.Add($update) } if ($updates.Count -eq 0) { Write-Output 'No applicable Windows updates found.' exit 0 } Write-Output "Downloading $($updates.Count) Windows update(s)." $downloader = $session.CreateUpdateDownloader() $downloader.Updates = $updates $downloadResult = $downloader.Download() Write-Output "Windows Update download result code: $($downloadResult.ResultCode)." if ($downloadResult.ResultCode -ne 2) { throw "Windows Update download did not fully succeed; result code: $($downloadResult.ResultCode)" } Write-Output "Installing $($updates.Count) Windows update(s)." $installer = $session.CreateUpdateInstaller() $installer.Updates = $updates $installer.ForceQuiet = $true $installResult = $installer.Install() Write-Output "Windows Update result code: $($installResult.ResultCode); reboot required: $($installResult.RebootRequired)." if ($installResult.ResultCode -ne 2) { throw "Windows Update install did not fully succeed; result code: $($installResult.ResultCode)" } exit 0`) } func DetectWindowsRebootRequired(ctx context.Context, runner Runner) (bool, error) { cmd := powerShell(`$paths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' ) $pending = $false foreach ($path in $paths) { if (Test-Path $path) { $pending = $true } } $sessionManager = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction SilentlyContinue if ($sessionManager.PendingFileRenameOperations) { $pending = $true } if ($pending) { 'true' } else { 'false' }`) res := runner.Run(ctx, cmd.Name, cmd.Args...) if res.Err != nil { return false, fmt.Errorf("%w: %s", res.Err, res.CombinedOutput()) } return containsLine(res.Stdout, "true"), nil } func RestorePointCommand(description string) CommandSpec { return powerShell(fmt.Sprintf(`$ErrorActionPreference='Stop' try { Enable-ComputerRestore -Drive "$env:SystemDrive\" -ErrorAction SilentlyContinue } catch {} $restorePolicy = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore' New-Item -Path $restorePolicy -Force | Out-Null Set-ItemProperty -Path $restorePolicy -Name SystemRestorePointCreationFrequency -Type DWord -Value 0 Checkpoint-Computer -Description %q -RestorePointType 'MODIFY_SETTINGS' Write-Output 'Restore point created: %s'`, description, description)) } func WinUtilCommand() CommandSpec { config := mustInstallAssetPath("assets/winutil-sane-default.json") return powerShell(`$ErrorActionPreference='Stop' $config = '` + psSingleQuote(config) + `' & ([ScriptBlock]::Create((irm 'https://christitus.com/win'))) -Config $config -Noui`) } func PowerPolicyCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' function Invoke-Native { param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments) & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } $stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env' New-Item -ItemType Directory -Force -Path $stateDir | Out-Null $policyScript = Join-Path $stateDir 'Apply-PowerPolicy.ps1' @' $ErrorActionPreference='Stop' function Invoke-Native { param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments) & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } $isLaptop = @(Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue).Count -gt 0 $onBattery = $false if ($isLaptop) { $batteries = @(Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue) $onBattery = ($batteries | Where-Object { $_.BatteryStatus -in 1, 4, 5 }).Count -gt 0 } if ($isLaptop -and $onBattery) { Invoke-Native 'powercfg.exe' @('/setactive', 'SCHEME_MAX') $lockSeconds = 120 } else { Invoke-Native 'powercfg.exe' @('/setactive', 'SCHEME_MIN') $lockSeconds = 300 } New-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Force | Out-Null Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -Type DWord -Value $lockSeconds '@ | Set-Content -Path $policyScript -Encoding UTF8 Invoke-Native 'powercfg.exe' @('/hibernate', 'on') New-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FlyoutMenuSettings' -Force | Out-Null Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FlyoutMenuSettings' -Name ShowHibernateOption -Type DWord -Value 1 foreach ($scheme in @('SCHEME_MIN', 'SCHEME_MAX')) { Invoke-Native 'powercfg.exe' @('/setacvalueindex', $scheme, 'SUB_VIDEO', 'VIDEOIDLE', '900') Invoke-Native 'powercfg.exe' @('/setdcvalueindex', $scheme, 'SUB_VIDEO', 'VIDEOIDLE', '300') Invoke-Native 'powercfg.exe' @('/setacvalueindex', $scheme, 'SUB_SLEEP', 'STANDBYIDLE', '0') Invoke-Native 'powercfg.exe' @('/setdcvalueindex', $scheme, 'SUB_SLEEP', 'STANDBYIDLE', '900') Invoke-Native 'powercfg.exe' @('/setacvalueindex', $scheme, 'SUB_BUTTONS', 'LIDACTION', '0') Invoke-Native 'powercfg.exe' @('/setdcvalueindex', $scheme, 'SUB_BUTTONS', 'LIDACTION', '1') Invoke-Native 'powercfg.exe' @('/setacvalueindex', $scheme, 'SUB_BUTTONS', 'PBUTTONACTION', '3') Invoke-Native 'powercfg.exe' @('/setdcvalueindex', $scheme, 'SUB_BUTTONS', 'PBUTTONACTION', '3') Invoke-Native 'powercfg.exe' @('/setacvalueindex', $scheme, 'SUB_BUTTONS', 'SBUTTONACTION', '1') Invoke-Native 'powercfg.exe' @('/setdcvalueindex', $scheme, 'SUB_BUTTONS', 'SBUTTONACTION', '1') } Invoke-Native 'powershell.exe' @('-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $policyScript) $taskArg = '-NoLogo -NoProfile -ExecutionPolicy Bypass -File "' + $policyScript + '"' $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $taskArg $logonTrigger = New-ScheduledTaskTrigger -AtLogOn $repeatTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650) Register-ScheduledTask -TaskName 'BootstrapWindowsEnv Power Policy Refresh' -Action $action -Trigger @($logonTrigger, $repeatTrigger) -RunLevel Highest -Force | Out-Null`) } func WindowsUpdatePolicyCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' $wu = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' $au = Join-Path $wu 'AU' New-Item -Path $wu -Force | Out-Null New-Item -Path $au -Force | Out-Null Set-ItemProperty -Path $wu -Name DeferFeatureUpdates -Type DWord -Value 1 Set-ItemProperty -Path $wu -Name DeferFeatureUpdatesPeriodInDays -Type DWord -Value 90 Set-ItemProperty -Path $wu -Name BranchReadinessLevel -Type DWord -Value 16 Set-ItemProperty -Path $wu -Name SetActiveHours -Type DWord -Value 1 Set-ItemProperty -Path $wu -Name ActiveHoursStart -Type DWord -Value 6 Set-ItemProperty -Path $wu -Name ActiveHoursEnd -Type DWord -Value 22 Set-ItemProperty -Path $au -Name AllowMUUpdateService -Type DWord -Value 1 $ux = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' New-Item -Path $ux -Force | Out-Null Set-ItemProperty -Path $ux -Name ActiveHoursStart -Type DWord -Value 6 Set-ItemProperty -Path $ux -Name ActiveHoursEnd -Type DWord -Value 22`) } func DeveloperModeCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' $unlock = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' New-Item -Path $unlock -Force | Out-Null Set-ItemProperty -Path $unlock -Name AllowDevelopmentWithoutDevLicense -Type DWord -Value 1 Set-ItemProperty -Path $unlock -Name AllowAllTrustedApps -Type DWord -Value 1 $devSettings = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeveloperSettings' New-Item -Path $devSettings -Force | Out-Null Set-ItemProperty -Path $devSettings -Name EnableSudo -Type DWord -Value 1 Set-ItemProperty -Path $devSettings -Name SudoMode -Type DWord -Value 1 if (Get-Command sudo.exe -ErrorAction SilentlyContinue) { sudo.exe config --enable normal if ($LASTEXITCODE -ne 0) { throw "sudo.exe config failed with exit code $LASTEXITCODE" } } else { Write-Output 'sudo.exe is not present on this Windows build; registry preference was written for builds that support Windows sudo.' }`) } func ExplorerThemeCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' $classic = 'HKCU:\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32' New-Item -Path $classic -Force | Out-Null Set-Item -Path $classic -Value '' $theme = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' New-Item -Path $theme -Force | Out-Null Set-ItemProperty -Path $theme -Name AppsUseLightTheme -Type DWord -Value 0 Set-ItemProperty -Path $theme -Name SystemUsesLightTheme -Type DWord -Value 0 Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue`) } func VivaldiDefaultBrowserCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' $progId = 'VivaldiHTM' $htmlProgId = $progId $roots = @('HKLM:\SOFTWARE\Clients\StartMenuInternet', 'HKCU:\SOFTWARE\Clients\StartMenuInternet') foreach ($root in $roots) { if (-not (Test-Path $root)) { continue } Get-ChildItem $root -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -like '*Vivaldi*' } | ForEach-Object { $cap = Join-Path $_.PSPath 'Capabilities' $url = Join-Path $cap 'URLAssociations' $file = Join-Path $cap 'FileAssociations' if (Test-Path $url) { $props = Get-ItemProperty $url if ($props.http) { $script:progId = $props.http } } if (Test-Path $file) { $props = Get-ItemProperty $file if ($props.'.html') { $script:htmlProgId = $props.'.html' } } } } $xml = Join-Path $env:ProgramData 'bootstrap_windows_env\DefaultAssociations.xml' New-Item -ItemType Directory -Force -Path (Split-Path $xml) | Out-Null @" "@ | Set-Content -Path $xml -Encoding UTF8 dism.exe /Online /Import-DefaultAppAssociations:$xml if ($LASTEXITCODE -ne 0) { throw "dism.exe default association import failed with exit code $LASTEXITCODE" } Write-Output 'Imported Vivaldi default browser associations. Windows may still require user confirmation for an existing signed-in profile.'`) } func StoreUpdateCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' winget upgrade --all --source msstore --accept-source-agreements --accept-package-agreements --disable-interactivity if ($LASTEXITCODE -ne 0) { Write-Output "Warning: winget Store app upgrade returned non-zero exit code: $LASTEXITCODE" } try { $mgr = Get-CimInstance -Namespace 'Root\cimv2\mdm\dmmap' -ClassName 'MDM_EnterpriseModernAppManagement_AppManagement01' -ErrorAction Stop Invoke-CimMethod -InputObject $mgr -MethodName UpdateScanMethod | Out-Null } catch { Write-Output "Store update scan fallback skipped: $($_.Exception.Message)" } try { Start-Process wsreset.exe -ArgumentList '-i' -WindowStyle Hidden -Wait } catch { Write-Output "wsreset fallback skipped: $($_.Exception.Message)" }`) } func TaskbarCommand() CommandSpec { return powerShell(`$ErrorActionPreference='Stop' function Invoke-TaskbarVerb { param([string]$Path, [string]$Pattern) if (-not (Test-Path $Path)) { return $false } $shell = New-Object -ComObject Shell.Application $folder = $shell.Namespace((Split-Path $Path)) $item = $folder.ParseName((Split-Path $Path -Leaf)) if (-not $item) { return $false } $verb = $item.Verbs() | Where-Object { ($_.Name -replace '&','') -match $Pattern } | Select-Object -First 1 if ($verb) { $verb.DoIt(); return $true } return $false } $failures = New-Object System.Collections.Generic.List[string] $edgePaths = @( "$env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" ) foreach ($edge in $edgePaths) { if ((Test-Path $edge) -and -not (Invoke-TaskbarVerb -Path $edge -Pattern 'Unpin from taskbar|Unpin from Taskbar')) { $failures.Add("could not unpin Edge from $edge") } } if (-not (Invoke-TaskbarVerb -Path "$env:windir\System32\Taskmgr.exe" -Pattern 'Pin to taskbar|Pin to Taskbar')) { $failures.Add('could not pin Task Manager to taskbar') } $settingsShortcut = Join-Path $env:ProgramData 'bootstrap_windows_env\Settings.lnk' New-Item -ItemType Directory -Force -Path (Split-Path $settingsShortcut) | Out-Null $wsh = New-Object -ComObject WScript.Shell $shortcut = $wsh.CreateShortcut($settingsShortcut) $shortcut.TargetPath = 'explorer.exe' $shortcut.Arguments = 'ms-settings:' $shortcut.IconLocation = "$env:windir\ImmersiveControlPanel\SystemSettings.exe" $shortcut.Save() if (-not (Invoke-TaskbarVerb -Path $settingsShortcut -Pattern 'Pin to taskbar|Pin to Taskbar')) { $failures.Add('could not pin Settings to taskbar') } if ($failures.Count -gt 0) { throw ($failures -join '; ') }`) } func mustInstallAssetPath(asset string) string { path, err := writeInstallAsset(asset) if err != nil { return asset } return path } func psSingleQuote(value string) string { return strings.ReplaceAll(value, `'`, `''`) } func containsLine(output, needle string) bool { for _, line := range stringsSplitLines(output) { if line == needle { return true } } return false } func stringsSplitLines(output string) []string { var lines []string for _, line := range strings.Split(strings.ReplaceAll(output, "\r\n", "\n"), "\n") { lines = append(lines, strings.TrimSpace(strings.ToLower(line))) } return lines }