Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

PowerShell Refresh

Posted on March 1, 2024March 1, 2024

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.

Manage and Report Active Directory, Exchange and Microsoft 365 with
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.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

6 thoughts on “PowerShell Refresh”

  1. Pingback: PowerShellGet Module 업데이트하기 – The PowerShell of Windows
  2. Pingback: Configuring a New Powershell Installation – Curated SQL
  3. Ľuboš Nikolíni says:
    March 12, 2024 at 8:07 am

    Hello Jeffery, it is really useful to have all these repetitive tasks under one umbrella! Thank you very much for sharing.

  4. Gyzmos says:
    March 18, 2024 at 5:23 am

    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?

    1. Jeffery Hicks says:
      March 18, 2024 at 9:02 am

      I use it on my Windows 10 lab machines as part of an automated build process.

  5. Pingback: PowerShell SnippetRace 11-12/2024 | PowerShell Usergroup Austria

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d