The other day on X, I was asked about what things I would setup or configure on a new PowerShell installation. This is something I actually have thought about and face all the time when I setup a new demo virtual machine. I had been meaning to build new tooling to meet this challenge, and the question provided the spark I needed to get off my butt and get it done.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Windows PowerShell Essentials
If you are running PowerShell 7, many of the critical items on my list are already addressed, so my code samples today will focus on Windows PowerShell 5.1 running on Windows 10 or Windows 11. The goal is to configure the Windows PowerShell environment for interactive use and code development.
Out of the box, Windows PowerShell ships with several features that have undergone significant changes. I want to update these features. The tricky thing is that because they are included with the operating system, you can't perform a simple update. For me, I want to make sure I have the latest version of the PSReadLine and Pester modules. I also want to use the newer Microsoft.PowerShell.PSResourceGet module which replaces the older PowerShellGet module. However, before I can install and use PSResourceGet, I need to update PowerShellGet.
Check to see what version you are using.
PS C:\> Get-Module PowerShellGet -ListAvailable
Directory: C:\Program Files\WindowsPowerShell\Modules
ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Script 1.0.0.1 PowerShellGet {Install-Module, Find-Modul...}
If you see this, you need to update the module. However, you can't update it because it wasn't installed using Install-Module
. This means you have to install as a new module.
Install-Module -Name PowerShellGet -Force -AllowClobber -repository PSGallery
Answer yes if prompted about updating the NuGet provider.
Depending on your system, you might need to adjust your TLS settings to communicate with the PowerShell Gallery.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Once the new version is installed, you either need to restart PowerShell to use the new version or manually remove and re-import it.
Remove-Module PowerShellGet
Import-Module PowerShellGet
``
Next, I can install PSResourceGet module using the older `Install-Module` command.
```powershell
Install-Module -Name Microsoft.PowerShell.PSResourceGet -Force -repository PSGallery
With the new module, I can refresh the Pester and PSReadLine modules. These too ship with Windows PowerShell on Windows 10 and 11 so I need tdo install them as new modules.
Install-Module -Name Pester -TrustRepository -repository PSGallery
Install-Module -Name PSReadLine -TrustRepository -repository PSGallery
The last minimal PowerShell refresh step is to update the help files.
Update-Help -Force
WinGet
The other setup task I would want to take is to add the necessary tools to my Windows 10 or Windows 11 environment related to PowerShell scripting. At a minimum this means installing:
- git
- VSCode
- The GitHub CLI
The best solution will be to use a package manager to install these tools. You can use whatever package manager you prefer, but I am going to use the Windows Package Manager, WinGet.
I could take the easy route and install the tools manually, but I want to automate the process, including installing Winget
. This gets slightly complicated. Winget
has several dependencies I have to account for. I can get them from Nuget.org but I'll need to add a new package source.
Register-PackageSource -Name Nuget.org -ProviderName NuGet -Force -ForceBootstrap -Location 'https://nuget.org/api/v2'
I can use this source to install the first dependency.
Install-Package -Name microsoft.ui.xaml -Source nuget.org -Force
This process doesn't install it like a piece of software. But I can get what I need from the package contents. The installed file has a .nuget
extension, but the file is a zip file. I can extract the contents to a folder get the file I need, which I can then install using Add-AppxPackage
.
Copy-Item -Path (Get-Package microsoft.ui.xaml).source -Destination $env:TEMP\microsoft.ui.xaml.zip
Expand-Archive $env:temp\microsoft.ui.xaml.zip -DestinationPath "$env:temp\ui" -Force
Add-AppxPackage $env:temp\ui\tools\appx\x64\release\Microsoft.UI.Xaml.2.8.appx
The other dependency I can download directly and add with Add-AppxPackage
.
Invoke-WebRequest -Uri'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' -OutFile $env:temp\VCLibs.appx
Add-AppxPackage $env:temp\VCLibs.appx
Once these dependencies are installed, I can install Winget
by downloading it from Github.
#get the current release
$uri = 'https://api.github.com/repos/microsoft/winget-cli/releases'
$get = Invoke-RestMethod -Uri $uri -Method Get -ErrorAction stop
#get the URL to the current release asset
$current = $get[0].assets | Where-Object name -Match 'msixbundle'
#define a path to save the file
$out = Join-Path -Path $env:temp -child $current.name
#download the file
Invoke-WebRequest -Uri $current.browser_download_url -OutFile $out
#install the file
Add-AppxPackage -Path $out
This is about a 250MB file so if you think you will be installing it often, you will want to save the file.
Packages
Now, I can install the Winget
packages. I use the package ID
. I want the installation to be silent and completely hands-free.
winget install --id git.git --silent --accept-package-agreements --accept-source-agreements --source winget
winget install --id github.cli --silent --accept-package-agreements --accept-source-agreements --source winget
winget install --id Microsoft.VisualStudioCode --silent --accept-package-agreements --accept-source-agreements --source winget
An Automated Solution
Naturally, I want to automate this entire process. So I wrote a PowerShell script file.
#requires -version 5.1
#requires -RunAsAdministrator
#PSRefresh.ps1
<#
Update key PowerShell components on a new Windows 10/11 installation.
This script is not intended for server operating systems. The script
should be run in an interactive console session and not in a remoting session.
You can modify this script to use a different package manager like Chocolatey.
If you use the Offline option, make sure your file names match the script.
This script is offered AS-IS and without warranty. Use at your own risk.
#>
#TODO: Add SupportsShouldProcess code
#TODO: Add proper error handling
[CmdletBinding()]
Param(
[Parameter(Position = 0,Mandatory,HelpMessage = 'The path to a configuration data file')]
[ValidateScript({ Test-Path -Path $_})]
[ValidatePattern('\.psd1$')]
[string]$ConfigurationData,
[Parameter(HelpMessage = 'Specify a location with previously installed Appx packages')]
[ValidateScript({ Test-Path -Path $_ })]
[string]$Offline
)
#this script should be run in the console, not the ISE or VSCode
if ($Host.name -ne 'ConsoleHost') {
Write-Warning 'This script should be run in the PowerShell console, not the ISE or VSCode'
return
}
#region Setup
Try {
$data = Import-PowerShellDataFile -Path $ConfigurationData -ErrorAction Stop
}
Catch {
Write-Warning "Failed to import $ConfigurationData"
Return
}
#define a list of winget package IDs
#$wingetPackages = @('Microsoft.VisualStudioCode', 'Git.Git', 'GitHub.cli', 'Microsoft.WindowsTerminal')
$wingetPackages = $data.wingetPackages
$PSModules = $data.PSModules
$Scope = $data.Scope
$vscExtensions = $data.vscExtensions
#install winget apps and additional PowerShell modules via background jobs
$jobs = @()
$installParams = @{
Scope = $Scope
Repository = 'PSGallery'
Force = $true
AllowClobber = $true
Name = $null
}
$progParams = @{
Activity = $MyInvocation.MyCommand
Status = 'Initializing'
CurrentOperation = 'Bootstrapping a NuGet provider update'
PercentComplete = 1
}
#set TLS just in case
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Progress @progParams
#Bootstrap Nuget provider update to avoid interactive prompts
[void](Install-PackageProvider -Name Nuget -ForceBootstrap -Force)
#endregion
#region Update PowerShellGet
$progParams.Status = "Module updates"
$progParams.CurrentOperation = "Updating PowerShellGet"
$progParams.PercentComplete = 10
Write-Progress @progParams
$get = Get-Module PowerShellGet -ListAvailable | Select-Object -First 1
if ($get.Version.major -eq 1) {
#Write-Host 'Installing the latest version of PowerShellGet' -ForegroundColor Yellow
$installParams.Name = 'PowerShellGet'
Install-Module @installParams
}
else {
#Write-Host 'Updating PowerShellGet' -ForegroundColor Yellow
Update-Module -Name PowerShellGet -Force
}
#reload PowerShellGet
Remove-Module PowerShellGet
Import-Module PowerShellGet
#endregion
#region Install Microsoft.PowerShell.PSResourceGet
$progParams.Status = "Module updates"
$progParams.CurrentOperation = "Microsoft.PowerShell.PSResourceGet"
$progParams.PercentComplete = 20
Write-Progress @progParams
#Write-Host 'Installing Microsoft.PowerShell.PSResourceGet' -ForegroundColor Yellow
$installParams.Name = 'Microsoft.PowerShell.PSResourceGet'
Install-Module @installParams
Import-Module Microsoft.PowerShell.PSResourceGet
#endregion
#region Install updated Modules
$progParams.Status = "Module updates"
$progParams.CurrentOperation = "PSReadLine"
$progParams.PercentComplete = 25
Write-Progress @progParams
#Write-Host 'Installing PSReadLine' -ForegroundColor Yellow
Install-PSResource -Name PSReadLine -Scope $Scope -Repository PSGallery -TrustRepository
$progParams.Status = "Module updates"
$progParams.CurrentOperation = "Pester - You may see a warning."
$progParams.PercentComplete = 30
Write-Progress @progParams
#Write-Host 'Installing Pester. You might see a warning.' -ForegroundColor Yellow
Install-PSResource -Name Pester -Scope $Scope -Repository PSGallery -TrustRepository
#endregion
#region install winget dependencies
$progParams.Status = "Installing Winget"
$progParams.CurrentOperation = "Processing dependencies"
$progParams.PercentComplete = 40
Write-Progress @progParams
#Write-Host 'Adding Nuget.org as a package source' -ForegroundColor Yellow
[void](Register-PackageSource -Name Nuget.org -ProviderName NuGet -Force -ForceBootstrap -Location 'https://nuget.org/api/v2')
#Write-Host 'Installing winget dependencies' -ForegroundColor Yellow
if ($Offline) {
Add-AppxPackage "$Offline\microsoft.ui.xaml.2.8.appx"
Add-AppxPackage "$Offline\VCLibs.appx"
}
else {
[void](Install-Package -Name microsoft.ui.xaml -Source nuget.org -Force)
Copy-Item -Path (Get-Package microsoft.ui.xaml).source -Destination $env:TEMP\microsoft.ui.xaml.zip
Expand-Archive $env:temp\microsoft.ui.xaml.zip -DestinationPath "$env:temp\ui" -Force
Add-AppxPackage $env:temp\ui\tools\appx\x64\release\Microsoft.UI.Xaml.2.8.appx
$uri = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx'
Invoke-WebRequest -Uri $uri -OutFile $env:temp\VCLibs.appx
Add-AppxPackage $env:temp\VCLibs.appx
}
#endregion
#region Install winget
#Write-Host 'Installing winget' -ForegroundColor Yellow
$progParams.Status = "Installing Winget"
$progParams.CurrentOperation = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
$progParams.PercentComplete = 50
Write-Progress @progParams
if ($Offline) {
Add-AppxPackage "$Offline\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
}
else {
#Winget is a 246MB download
$uri = 'https://api.github.com/repos/microsoft/winget-cli/releases'
$get = Invoke-RestMethod -Uri $uri -Method Get -ErrorAction stop
$current = $get[0].assets | Where-Object name -Match 'msixbundle'
#Write-Host "Downloading $($current.name)" -ForegroundColor Yellow
$out = Join-Path -Path $env:temp -child $current.name
Try {
Invoke-WebRequest -Uri $current.browser_download_url -OutFile $out -ErrorAction Stop
Add-AppxPackage -Path $out
}
Catch {
$_
}
}
#endregion
#region Install winget packages
$progParams.Status = "Installing packages via winget"
$pct = 50
foreach ($package in $wingetPackages) {
$progParams.CurrentOperation = $package
$progParams.PercentComplete = $pct+=2
Write-Progress @progParams
$jobs+= Start-Job -Name $package -ScriptBlock {
#This script does not take scope into account for Winget installations.
#You might want to change that.
Param($package)
winget install --id $package --silent --accept-package-agreements --accept-source-agreements --source winget
} -ArgumentList $package
#Write-Host "Installing $package" -ForegroundColor Yellow
#winget install --id $package --silent --accept-package-agreements --accept-source-agreements --source winget
} #foreach package
#endregion
#region install additional PowerShell modules
if ($PSModules) {
$progParams.Status = "Installing additional PowerShell modules"
foreach ($Mod in $PSModules) {
$progParams.CurrentOperation = $Mod
$progParams.PercentComplete = $pct+=2
Write-Progress @progParams
$jobs+= Start-Job -Name $Mod -ScriptBlock {
#This script does not take scope into account for Winget installations.
#You might want to change that.
Param($ModuleName,$scope)
Import-Module Microsoft.PowerShell.PSResourceGet
Install-PSResource -Name $ModuleName -Scope $Scope -Repository PSGallery -AcceptLicense -TrustRepository -Quiet
} -ArgumentList $Mod,$Scope
} #foreach module
}
#endregion
#region install VSCode extensions
if ($vscExtensions) {
$progParams.Status = "Installing VSCode extensions"
$progParams.PercentComplete = $pct+=2
foreach ($Extension in $vscExtensions) {
$progParams.CurrentOperation = $Extension
Write-Progress @progParams
$jobs+= Start-Job -Name $Extension -ScriptBlock {
Param($Name)
&"$HOME\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd" --install-extension $Name --force
} -ArgumentList $Extension
}
}
#endregion
#region Update help
$progParams.Status = "Updating Help. Some errors are to be expected."
$progParams.CurrentOperation = "Update-Help -Force"
$progParams.PercentComplete = $pct+=5
Write-Progress @progParams
#Write-Host 'Updating PowerShell help. Some errors are to be expected.' -ForegroundColor Yellow
Update-Help -Force
#endregion
#region Wait for end
$progParams.Status = "Waiting for $($jobs.count) background jobs to complete"
$progParams.CurrentOperation = "Wait-Job"
$progParams.PercentComplete = $pct+=2
Write-Progress @progParams
$jobs | Wait-Job | Select-Object Name,State
Write-Progress -Activity $progParams.Activity -Completed -Status "All tasks completed." -PercentComplete 100
$msg = @'
Refresh is complete. You might also want to install the following modules using Install-PSResource:
Microsoft.Winget.Client
Microsoft.PowerShell.SecretStore
Microsoft.PowerShell.SecretManagement
Platyps
And the following packages via Winget:
Microsoft.PowerToys
Microsoft.PowerShell
You will need to configure VSCode with your preferred extensions and settings or configure
it to synch your saved settings.
Please restart your PowerShell session.
'@
Write-Host $msg -ForegroundColor Green
#endregion
#EOF
The script uses Write-Progress
, but I've left the original Write-Host
commands in the script commented out.
In the initial versions of the script, I included additional PowerShell modules and packages directly in the code. Some items, like installing Pester and PSReadLine are special cases so I don't have problem with that code. But everything else can be moved to a configuration data file. This is a good example of the concept of separating the data you need to use from the code itself.
#PSRefresh.psd1
#PSModules should be new modules to install
#Scope can be 'CurrentUser' or 'AllUsers'
#vscExtensions are Visual Studio Code extensions to install
@{
wingetPackages = @(
'Microsoft.VisualStudioCode',
'Git.Git',
'GitHub.cli',
'Microsoft.WindowsTerminal',
'github.githubdesktop',
'Microsoft.PowerShell'
)
PSModules = @("PSScriptTools","PSProjectStatus","PSStyle","Platyps")
vscExtensions = @(
'github.copilot',
'github.copilot-chat',
'github.remotehub',
'ms-vscode.powershell',
'davidanson.vscode-markdownlint',
'inu1255.easy-snippet',
'gruntfuggly.todo-tree'
)
Scope = 'AllUsers'
Version = '1.2.0'
}
My script will also install VSCode extensions. I included this more as a demonstration than anything. Normally, I synchronize my VSCode settings across installations, but it is possible I'm setting up a new demo environment where I'm not going to login so this could be useful.
The configuration file is mandatory.
c:\scripts\psrefresh.ps1 -configurationdata c:\scripts\psrefresh.psd1
If I decide to change modules or packages, all I have to do is edit the configuration file. I don't have to worry about messing up my code with revisions.
The other feature in my script is the ability to install the Appx packages offline. The Winget
dependencies don't change often so I can save the files that I've downloaded. Likewise, I can periodically download the latest Winget
release for future installations. The packages should go in the same folder. Be sure to use the same file names as in the script.
To use, all I need to do is specify the path to the saved files.
c:\scripts\psrefresh.ps1 -configurationdata c:\scripts\psrefresh.psd1 -offline d:\saved
Background Jobs
The other feature of the script to note is the use of background jobs. Once the core steps are out of the way, installing additional modules and packages can happen in parallel. The easiest way in Windows PowerShell is to run each activity in a background job.
foreach ($Mod in $PSModules) {
$progParams.CurrentOperation = $Mod
$progParams.PercentComplete = $pct+=2
Write-Progress @progParams
$jobs+= Start-Job -Name $Mod -ScriptBlock {
Param($ModuleName,$scope)
Import-Module Microsoft.PowerShell.PSResourceGet
Install-PSResource -Name $ModuleName -Scope $Scope -Repository PSGallery -AcceptLicense -TrustRepository -Quiet
} -ArgumentList $Mod,$Scope
} #foreach module
The jobs run while PowerShell is updated. The script waits for all jobs to complete before displaying a completion message.
```powershell
$jobs | Wait-Job | Select-Object Name,State
$msg = @'
Refresh is complete. You might also want to install the following modules using Install-PSResource:
Microsoft.Winget.Client
Microsoft.PowerShell.SecretStore
Microsoft.PowerShell.SecretManagement
Platyps
And the following packages via Winget:
Microsoft.PowerToys
Microsoft.PowerShell
You will need to configure VSCode with your preferred extensions and settings or configure
it to synch your saved settings.
Please restart your PowerShell session.
'@
Write-Host $msg -ForegroundColor Green
Your Turn
You are welcome to give my code a try. I have posted the files as a GitHub gist, which I'll try to keep updated. The files are offered AS-IS for educational purposes as much as anything. The script is not 100% production-ready and lacks error handling. And of course, the extra modules and packages are things that I would want to install it. I'd love to hear what you think.
Hello Jeffery, it is really useful to have all these repetitive tasks under one umbrella! Thank you very much for sharing.
Thanks for sharing this setup, nice!
Small remark: “Windows Package Manager winget command-line tool is available on Windows 11 and modern versions of Windows 10 as a part of the App Installer”
https://learn.microsoft.com/en-us/windows/package-manager/winget/
As it is inbox why do the extra work to install it?
I use it on my Windows 10 lab machines as part of an automated build process.