Skip to content

Commit f5eaa06

Browse files
committed
✨feat: add Save-MaesterOffline function for offline installation of PowerShell modules
1 parent a9ef14f commit f5eaa06

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

General/Save-MaesterOffline.ps1

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
function Save-MaesterOffline {
2+
<#
3+
.SYNOPSIS
4+
Download local copies of Maester and its dependencies for use on systems that cannot access the PowerShell Gallery.
5+
6+
.DESCRIPTION
7+
This function downloads the Maester module and its dependencies to a specified directory, allowing for offline
8+
installation on systems without access to the PowerShell Gallery. Once downloaded the modules can be copied to
9+
the target system and installed using Install-Module with the -Path parameter.
10+
11+
.PARAMETER DestinationPath
12+
The directory to download and save the required PowerShell modules in.
13+
14+
.EXAMPLE
15+
Save-MaesterOffline -DestinationPath ~/Downloads/Maester
16+
17+
.NOTES
18+
Author: Sam Erde (@SamErde)
19+
Company: Sentinel Technologies, Inc
20+
Version: 1.0.0
21+
Date: 2025-09-09
22+
23+
#>
24+
[CmdletBinding()]
25+
param (
26+
# Directory to download and save the required PowerShell modules in.
27+
[Parameter(HelpMessage = 'The path to an existing directory to download and save the required PowerShell modules in.')]
28+
[ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid } )]
29+
[string] $DestinationPath = $PWD.Path,
30+
31+
# Switch to create a ZIP file of the downloaded modules.
32+
[Parameter(HelpMessage = 'Create a ZIP file of the downloaded modules.')]
33+
[switch] $CreateZip
34+
)
35+
36+
# Ensure the destination path exists, or try to create it, if necessary.
37+
if (Test-Path -Path $DestinationPath -PathType Container) {
38+
Write-Verbose "Using existing directory: $DestinationPath"
39+
} else {
40+
try {
41+
New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
42+
Write-Verbose "Created directory: $DestinationPath"
43+
} catch {
44+
Write-Error "Failed to create directory: $DestinationPath. Error: $_"
45+
return
46+
}
47+
}
48+
49+
# Check if the Microsoft.PowerShell.PSResourceGet module is available and attempt to install it if necessary. If
50+
# the PSResourceGet module is not available, fall back to using Save-Module.
51+
$PSResourceGetInstalled = $false
52+
if ( (Get-Command -Name Save-PSResource -ErrorAction SilentlyContinue) ) {
53+
$PSResourceGetInstalled = $true
54+
} else {
55+
Write-Host "The 'Microsoft.PowerShell.PSResourceGet' module is not available. Attempting to install it from the PowerShell Gallery..."
56+
try {
57+
Install-Module -Name 'Microsoft.PowerShell.PSResourceGet' -Scope CurrentUser -Force -ErrorAction Stop
58+
Import-Module -Name 'Microsoft.PowerShell.PSResourceGet' -Force -ErrorAction Stop
59+
Write-Host "Successfully installed and imported the 'Microsoft.PowerShell.PSResourceGet' module."
60+
$PSResourceGetInstalled = $true
61+
} catch {
62+
Write-Error "Failed to install or import the 'Microsoft.PowerShell.PSResourceGet' module. Error: $_"
63+
return
64+
}
65+
}
66+
67+
# List the required module names and versions.
68+
$RequiredModules = @(
69+
@{
70+
Name = 'Pester'
71+
Prerelease = $false
72+
Version = [version]'5.7.1'
73+
},
74+
@{
75+
Name = 'Maester'
76+
Prerelease = $true
77+
Version = $null # Get the latest
78+
},
79+
@{
80+
Name = 'Az.Accounts'
81+
Prerelease = $false
82+
Version = $null # Get the latest
83+
},
84+
@{
85+
Name = 'ExchangeOnlineManagement'
86+
Prerelease = $false
87+
Version = $null # Get the latest
88+
},
89+
@{
90+
Name = 'Microsoft.Graph.Authentication'
91+
Prerelease = $false
92+
Version = $null # Get the latest. Just don't get 2.26.*!
93+
},
94+
@{
95+
Name = 'MicrosoftTeams'
96+
Prerelease = $false
97+
Version = $null # Get the latest
98+
}
99+
)
100+
101+
# Track installed modules.
102+
$InstalledModules = @()
103+
104+
if ($PSResourceGetInstalled) {
105+
Write-Host "Using 'Save-PSResource' (Microsoft.PowerShell.PSResourceGet) to download modules.`n" -ForegroundColor White
106+
} else {
107+
Write-Host "Using 'Save-Module' (PowerShellGet) to download modules.`n" -ForegroundColor White
108+
}
109+
110+
# Download the required modules into the DestinationPath.
111+
foreach ($Module in $RequiredModules) {
112+
$Name = $Module.Name
113+
$Version = $Module.Version
114+
$Prerelease = $Module.Prerelease
115+
116+
try {
117+
Write-Host "Downloading module: $Name, Version: $Version, Prerelease: $Prerelease" -ForegroundColor Cyan
118+
if ($PSResourceGetInstalled) {
119+
#try {
120+
if ($Version) {
121+
Save-PSResource -Name $Name -Path $DestinationPath -Version $Version -Prerelease:$Prerelease -SkipDependencyCheck
122+
} else {
123+
Write-Verbose "Getting latest version"
124+
Save-PSResource -Name $Name -Path $DestinationPath -Prerelease:$Prerelease -SkipDependencyCheck
125+
}
126+
$InstalledModules += $Name
127+
Write-Host "Successfully downloaded module: $Name" -ForegroundColor Green
128+
} else {
129+
if ($Version) {
130+
Save-Module -Name $Name -Path $DestinationPath -MinimumVersion $Version -AllowPrerelease:$Prerelease
131+
} else {
132+
Save-Module -Name $Name -Path $DestinationPath -AllowPrerelease:$Prerelease
133+
}
134+
$InstalledModules += $Name
135+
Write-Host "Successfully downloaded module: $Name" -ForegroundColor Green
136+
}
137+
} catch {
138+
Write-Error "Failed to download module: $Name. Error: $_"
139+
}
140+
}
141+
142+
# Summary of downloaded modules.
143+
if ($InstalledModules.Count -gt 0) {
144+
Write-Host "`nDownloaded modules to $DestinationPath`n"
145+
$InstalledModules | ForEach-Object -Process { Write-Host "`t$_" } -End "`n"
146+
} else {
147+
Write-Warning 'No modules were downloaded.'
148+
}
149+
150+
# Create a ZIP file of the downloaded modules if requested.
151+
if ($CreateZip.IsPresent -and $InstalledModules.Count -gt 0) {
152+
$ZipPath = Join-Path -Path $DestinationPath -ChildPath 'MaesterModuleWithDependencies.zip'
153+
try {
154+
# Remove old ZIP file if it exists already.
155+
if (Test-Path -Path $ZipPath) {
156+
Remove-Item -Path $ZipPath -Force
157+
Write-Verbose "Removed existing ZIP file: $ZipPath"
158+
}
159+
Compress-Archive -Path (Join-Path -Path $DestinationPath -ChildPath '*') -DestinationPath $ZipPath -Force
160+
Write-Host "`nCreated ZIP file: $ZipPath" -ForegroundColor Green
161+
} catch {
162+
Write-Error "Failed to create ZIP file: $ZipPath. Error: $_"
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)