Skip to content

Instantly share code, notes, and snippets.

@acepace
Forked from jessfraz/boxstarter.ps1
Last active June 7, 2026 10:46
Show Gist options
  • Select an option

  • Save acepace/d71a71c9cec4bed1ec1da0a6bd7b9a12 to your computer and use it in GitHub Desktop.

Select an option

Save acepace/d71a71c9cec4bed1ec1da0a6bd7b9a12 to your computer and use it in GitHub Desktop.
Boxstarter Commands for a new Windows box.
#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