From bbeb0e6d77eed9bc9059578221c55aa138522c1b Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:30:32 +0100 Subject: [PATCH 1/7] Fix encoding: replace em-dashes with ASCII hyphens for Windows compatibility Made-with: Cursor --- module/Private/IntuneEnrollmentFlows.ps1 | 132 +++++++++++------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/module/Private/IntuneEnrollmentFlows.ps1 b/module/Private/IntuneEnrollmentFlows.ps1 index 7f06472..a85c80a 100644 --- a/module/Private/IntuneEnrollmentFlows.ps1 +++ b/module/Private/IntuneEnrollmentFlows.ps1 @@ -6,7 +6,7 @@ $script:AllGroups = @{} $script:GroupMemberTypeCache = $null # Shared helper: convert boolean/null to Yes/No/dash -$script:ToYesNo = { param($v) if ($null -eq $v) { "—" } elseif ($v -eq $true) { "Yes" } elseif ($v -eq $false) { "No" } else { $v.ToString() } } +$script:ToYesNo = { param($v) if ($null -eq $v) { "-" } elseif ($v -eq $true) { "Yes" } elseif ($v -eq $false) { "No" } else { $v.ToString() } } # Shared lookup tables for OData assignment type <-> friendly name $script:ODataToFriendly = @{ @@ -109,9 +109,9 @@ function Get-AutopilotProfileConfigByDisplayName { $autopilotProfile = $profiles | Where-Object { $_.displayName -eq $DisplayName } | Select-Object -First 1 if (-not $autopilotProfile) { return $null } return @{ - DeviceNameTemplate = if ($autopilotProfile.deviceNameTemplate) { $autopilotProfile.deviceNameTemplate } else { "—" } - Language = if ($autopilotProfile.language) { $autopilotProfile.language } else { "—" } - Locale = if ($autopilotProfile.locale) { $autopilotProfile.locale } else { "—" } + DeviceNameTemplate = if ($autopilotProfile.deviceNameTemplate) { $autopilotProfile.deviceNameTemplate } else { "-" } + Language = if ($autopilotProfile.language) { $autopilotProfile.language } else { "-" } + Locale = if ($autopilotProfile.locale) { $autopilotProfile.locale } else { "-" } } } catch { return $null } @@ -145,8 +145,8 @@ function Get-EspConfigByDisplayName { $esp = if ($rawEsp.PSObject.Properties['value']) { $rawEsp.value } else { $rawEsp } if (-not $esp) { return $null } $toYesNo = $script:ToYesNo - $desc = if ($esp.description) { $esp.description } else { "—" } - $timeoutVal = $esp.installProgressTimeoutInMinutes; $timeoutMinutes = if ($null -ne $timeoutVal) { [int]$timeoutVal } else { $null }; $timeoutDisplay = if ($null -ne $timeoutMinutes) { "$timeoutMinutes" } else { "—" } + $desc = if ($esp.description) { $esp.description } else { "-" } + $timeoutVal = $esp.installProgressTimeoutInMinutes; $timeoutMinutes = if ($null -ne $timeoutVal) { [int]$timeoutVal } else { $null }; $timeoutDisplay = if ($null -ne $timeoutMinutes) { "$timeoutMinutes" } else { "-" } $showProgressVal = $esp.showInstallationProgress; $showProgress = & $toYesNo $showProgressVal $trackVal = $esp.trackInstallProgressForAutopilotOnly; $trackAutopilotOnly = & $toYesNo $trackVal $qualityVal = $esp.installQualityUpdates; $installQuality = & $toYesNo $qualityVal @@ -154,7 +154,7 @@ function Get-EspConfigByDisplayName { $allowUseVal = $esp.allowDeviceUseOnInstallFailure; $allowUse = & $toYesNo $allowUseVal $allowLogVal = $esp.allowLogCollectionOnInstallFailure; $allowLog = & $toYesNo $allowLogVal $allowNonBlockVal = $esp.allowNonBlockingAppInstallation; $allowNonBlocking = & $toYesNo $allowNonBlockVal - $customErr = if ($esp.customErrorMessage) { $esp.customErrorMessage.Trim() } else { "" }; $customErrorMessage = if ($customErr) { $customErr } else { "—" } + $customErr = if ($esp.customErrorMessage) { $esp.customErrorMessage.Trim() } else { "" }; $customErrorMessage = if ($customErr) { $customErr } else { "-" } $showCustomMessage = if ($customErr) { "Yes" } else { "No" } $selectedIds = [System.Collections.ArrayList]::new() $rawSelected = if ($esp.PSObject.Properties['selectedMobileAppIds']) { $esp.selectedMobileAppIds } elseif ($esp.PSObject.Properties['SelectedMobileAppIds']) { $esp.SelectedMobileAppIds } else { $null } @@ -505,8 +505,8 @@ function Get-DetailedPolicyAssignmentsFromExpanded { return $detailedAssignments } -# Cloud PC policies: Use the same approach as manual calls: (1) GET policy?$expand=assignments with default SDK response so assignments is Hashtable like { 28f33e7b-... }; (2) each key is the group ID — GET groups/{id} for displayName/rule. No -OutputType Json so we get the native Hashtable. -# Normalize a group so membershipRule is only set for Dynamic groups (Assigned groups may have license info in membershipRule from API — we show empty). +# Cloud PC policies: Use the same approach as manual calls: (1) GET policy?$expand=assignments with default SDK response so assignments is Hashtable like { 28f33e7b-... }; (2) each key is the group ID - GET groups/{id} for displayName/rule. No -OutputType Json so we get the native Hashtable. +# Normalize a group so membershipRule is only set for Dynamic groups (Assigned groups may have license info in membershipRule from API - we show empty). function Normalize-EntraGroupForDisplay { param([Parameter(Mandatory)][object]$Group) if (-not $Group) { return $null } @@ -748,14 +748,14 @@ function Get-CloudPCUserSettingsConfigByDisplayName { $toYesNo = $script:ToYesNo - $name = if ($policy.displayName) { $policy.displayName } else { "—" } + $name = if ($policy.displayName) { $policy.displayName } else { "-" } $selfServiceEnabled = & $toYesNo $policy.selfServiceEnabled $localAdminEnabled = & $toYesNo $policy.localAdminEnabled $resetEnabled = & $toYesNo $policy.resetEnabled # Restore Point Settings $restorePointSetting = $policy.restorePointSetting - $frequencyType = "—" + $frequencyType = "-" if ($restorePointSetting -and $restorePointSetting.frequencyType) { $freqType = $restorePointSetting.frequencyType $frequencyType = switch ($freqType) { @@ -771,12 +771,12 @@ function Get-CloudPCUserSettingsConfigByDisplayName { default { $freqType } } } - $userRestoreEnabled = if ($restorePointSetting -and $null -ne $restorePointSetting.userRestoreEnabled) { & $toYesNo $restorePointSetting.userRestoreEnabled } else { "—" } + $userRestoreEnabled = if ($restorePointSetting -and $null -ne $restorePointSetting.userRestoreEnabled) { & $toYesNo $restorePointSetting.userRestoreEnabled } else { "-" } # Cross Region Disaster Recovery Settings $drSetting = $policy.crossRegionDisasterRecoverySetting $drEnabled = "None" - $userInitiatedDRAllowed = "—" + $userInitiatedDRAllowed = "-" if ($drSetting) { if ($drSetting.crossRegionDisasterRecoveryEnabled -eq $true) { $drEnabled = "Enabled" @@ -786,7 +786,7 @@ function Get-CloudPCUserSettingsConfigByDisplayName { # Notification Settings $notificationSetting = $policy.notificationSetting - $restartPromptsDisabled = if ($notificationSetting -and $null -ne $notificationSetting.restartPromptsDisabled) { & $toYesNo $notificationSetting.restartPromptsDisabled } else { "—" } + $restartPromptsDisabled = if ($notificationSetting -and $null -ne $notificationSetting.restartPromptsDisabled) { & $toYesNo $notificationSetting.restartPromptsDisabled } else { "-" } return @{ Name = $name @@ -912,7 +912,7 @@ function Get-CloudPCPolicyGroupInfoInternal { if (-not $policyDisplayName) { $policyDisplayName = "Policy $PolicyId" } if ($groupIds.Count -eq 0) { if ($DebugMode) { Write-Host " [DEBUG] No group IDs found. Returning 'Not Assigned'" -ForegroundColor DarkYellow } - return [PSCustomObject]@{ PolicyId = $PolicyId; PolicyName = $policyDisplayName; GroupId = $null; GroupName = 'Not Assigned'; GroupType = '—'; MembershipRule = '—' } + return [PSCustomObject]@{ PolicyId = $PolicyId; PolicyName = $policyDisplayName; GroupId = $null; GroupName = 'Not Assigned'; GroupType = '-'; MembershipRule = '-' } } $results = @() @@ -929,7 +929,7 @@ function Get-CloudPCPolicyGroupInfoInternal { } if (-not $group) { if ($DebugMode) { Write-Host " [DEBUG] Adding assignment with group ID only (lookup failed)" -ForegroundColor DarkGray } - $results += [PSCustomObject]@{ PolicyId = $PolicyId; PolicyName = $policyDisplayName; GroupId = $gid; GroupName = "Group (ID: $gid)"; GroupType = '—'; MembershipRule = '—' } + $results += [PSCustomObject]@{ PolicyId = $PolicyId; PolicyName = $policyDisplayName; GroupId = $gid; GroupName = "Group (ID: $gid)"; GroupType = '-'; MembershipRule = '-' } continue } $normalized = Normalize-EntraGroupForDisplay -Group $group @@ -2052,23 +2052,23 @@ function Get-ArchitectureDiagramFragment { } } $deviceEnc = [System.Net.WebUtility]::HtmlEncode($DeviceName) - $intuneIdEnc = if ($IntuneDeviceId) { [System.Net.WebUtility]::HtmlEncode($IntuneDeviceId) } else { "—" } - $entraIdEnc = if ($EntraDeviceId) { [System.Net.WebUtility]::HtmlEncode($EntraDeviceId) } else { "—" } + $intuneIdEnc = if ($IntuneDeviceId) { [System.Net.WebUtility]::HtmlEncode($IntuneDeviceId) } else { "-" } + $entraIdEnc = if ($EntraDeviceId) { [System.Net.WebUtility]::HtmlEncode($EntraDeviceId) } else { "-" } $deviceTableRow = "$deviceEnc$intuneIdEnc$entraIdEnc" $groupTableRows = "" if (-not $DeviceGroupDetails -or $DeviceGroupDetails.Count -eq 0) { - $groupTableRows = "—" + $groupTableRows = "-" } else { foreach ($gd in $DeviceGroupDetails) { $encName = [System.Net.WebUtility]::HtmlEncode($gd.DisplayName) $encType = [System.Net.WebUtility]::HtmlEncode($gd.GroupType) - $ruleCell = if ($gd.MembershipRule) { $gd.MembershipRule } else { "—" } + $ruleCell = if ($gd.MembershipRule) { $gd.MembershipRule } else { "-" } $groupTableRows += "$encName$encType$ruleCell" } } - $autopilotName = if ($autopilotPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($autopilotPolicies[0]) } else { "— None —" } - $espName = if ($espPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($espPolicies[0]) } else { "— None —" } + $autopilotName = if ($autopilotPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($autopilotPolicies[0]) } else { "- None -" } + $espName = if ($espPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($espPolicies[0]) } else { "- None -" } $cloudPCSuffix = if ($IsCloudPC) { ", Cloud PC Prov($($cloudPCProvisioningPolicies.Count)), Cloud PC User($($cloudPCUserSettingsPolicies.Count))" } else { "" } $deviceSetupSub = "Configuration($($configDevicePolicies.Count)), Security Baselines($($securityBaselineDevicePolicies.Count)), Compliance($($complianceDevicePolicies.Count)), Apps($($appDevicePolicies.Count))$cloudPCSuffix" $accountSetupSub = "Configuration($($configUserPolicies.Count)), Security Baselines($($securityBaselineUserPolicies.Count)), Compliance($($complianceUserPolicies.Count)), Apps($($appUserPolicies.Count))$cloudPCSuffix" @@ -2106,7 +2106,7 @@ function Get-ArchitectureDiagramFragment { "@) [void]$sb.AppendLine('') if ($IsCloudPC) { - $cloudPCProvName = if ($cloudPCProvisioningPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($cloudPCProvisioningPolicies[0]) } else { "— None —" } + $cloudPCProvName = if ($cloudPCProvisioningPolicies.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($cloudPCProvisioningPolicies[0]) } else { "- None -" } [void]$sb.AppendLine(@"
Cloud PC Provisioning
@@ -2240,11 +2240,11 @@ function Get-AppliedFlowHtmlFragment { } $deviceEnc = [System.Net.WebUtility]::HtmlEncode($DeviceName) function Get-FirstAssignmentAndFilter($policyName, [array]$eval) { - if (-not $eval -or $eval.Count -eq 0) { return @("—", "—") } + if (-not $eval -or $eval.Count -eq 0) { return @("-", "-") } # Prefer a row with a real assignment (so Cloud PC / others show target when both "Not Assigned" and assigned rows exist) $a = $eval | Where-Object { $_.PolicyName -eq $policyName -and $_.AssignmentType -ne "Not Assigned" } | Select-Object -First 1 if (-not $a) { $a = $eval | Where-Object { $_.PolicyName -eq $policyName } | Select-Object -First 1 } - if (-not $a) { return @("—", "—") } + if (-not $a) { return @("-", "-") } $target = if ($a.AssignmentType -match "^(All Devices|All Users)$") { $a.AssignmentType } else { $a.TargetName } $filterD = "No Filter" if ($a.FilterName -and $a.FilterName -ne "No Filter" -and $a.FilterName -ne "Filter Not Found") { @@ -2269,12 +2269,12 @@ function Get-AppliedFlowHtmlFragment { return @($target, $filterD) } function Get-FirstAssignmentGroupTypeRule($policyName, [array]$eval) { - if (-not $eval -or $eval.Count -eq 0) { return @("Not Assigned", "—", "No Filter") } + if (-not $eval -or $eval.Count -eq 0) { return @("Not Assigned", "-", "No Filter") } $a = $eval | Where-Object { $_.PolicyName -eq $policyName -and $_.AssignmentType -ne "Not Assigned" } | Select-Object -First 1 if (-not $a) { $a = $eval | Where-Object { $_.PolicyName -eq $policyName } | Select-Object -First 1 } - if (-not $a) { return @("Not Assigned", "—", "No Filter") } + if (-not $a) { return @("Not Assigned", "-", "No Filter") } $groupName = if ($a.AssignmentType -match "^(All Devices|All Users)$") { $a.AssignmentType } else { $a.TargetName } - $typeLabel = "—" + $typeLabel = "-" $rule = if ($a.FilterName -and $a.FilterName -ne "No Filter" -and $a.FilterName -ne "Filter Not Found") { $a.FilterName } else { "No Filter" } if ($a.GroupId -and $script:AllGroups.Count -gt 0) { $grp = $script:AllGroups[$a.GroupId] @@ -2283,7 +2283,7 @@ function Get-AppliedFlowHtmlFragment { $isDynamic = $grp.groupTypes -and (@($grp.groupTypes) -contains 'DynamicMembership') $typeLabel = if ($isDynamic) { "Dynamic" } else { "Static" } # Only show membership rule for Dynamic groups; for Static use empty - if ($isDynamic -and $grp.membershipRule) { $rule = $grp.membershipRule } else { $rule = "—" } + if ($isDynamic -and $grp.membershipRule) { $rule = $grp.membershipRule } else { $rule = "-" } } } return @($groupName, $typeLabel, $rule) @@ -2347,7 +2347,7 @@ function Get-AppliedFlowHtmlFragment { function Write-FlowStepLikeReference($stepId, $stepClass, $title, $iconClass, $list, [array]$eval, $stepNameLabel, $categoryTitle, $col1Header, [string]$ConfigSectionHtml = "", [switch]$UseGroupTypeRuleTable) { $t = [System.Net.WebUtility]::HtmlEncode($title) $n = $list.Count - $stepName = if ($list.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($list[0]) } else { "— None —" } + $stepName = if ($list.Count -gt 0) { [System.Net.WebUtility]::HtmlEncode($list[0]) } else { "- None -" } # Don't show step-name for any policy type - it's redundant with the header and button $stepNameLine = "" $btnText = if ($stepId -eq "flow-autopilot") { "Expand Autopilot Profile Details" } elseif ($stepId -eq "flow-esp") { "Expand Enrollment Status Page Configuration" } else { "Expand " + $title + " (" + $n + ")" } @@ -2366,9 +2366,9 @@ $ConfigSectionHtml "@ } $autopilotConfig = if ($autopilotPolicies.Count -gt 0) { Get-AutopilotProfileConfigByDisplayName -DisplayName $autopilotPolicies[0] } else { $null } - $apDeviceName = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.DeviceNameTemplate) } else { "—" } - $apLanguage = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.Language) } else { "—" } - $apLocale = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.Locale) } else { "—" } + $apDeviceName = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.DeviceNameTemplate) } else { "-" } + $apLanguage = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.Language) } else { "-" } + $apLocale = if ($autopilotConfig) { [System.Net.WebUtility]::HtmlEncode($autopilotConfig.Locale) } else { "-" } $autopilotConfigSection = @"
@@ -2382,8 +2382,8 @@ $ConfigSectionHtml
"@ $espConfig = if ($espPolicies.Count -gt 0) { Get-EspConfigByDisplayName -DisplayName $espPolicies[0] } else { $null } - $espDesc = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.Description) } else { "—" } - $espShowProgress = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowInstallationProgress) } else { "—" } + $espDesc = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.Description) } else { "-" } + $espShowProgress = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowInstallationProgress) } else { "-" } $espRequiredAppsMode = "All" $espSelectedAppsHtml = "" $selectedAppIds = @() @@ -2404,16 +2404,16 @@ $ConfigSectionHtml $espRequiredAppsModeEnc = [System.Net.WebUtility]::HtmlEncode($espRequiredAppsMode) $espBlockDeviceUseHtml = "
Block device use until required apps are installed if they are assigned to the user/device:
$espRequiredAppsModeEnc
" if ($espRequiredAppsMode -eq "Selected") { $espBlockDeviceUseHtml += $espSelectedAppsHtml } - $espTimeout = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.InstallProgressTimeout) } else { "—" } - $espShowCustomMessage = if ($espConfig -and $espConfig.ShowCustomMessageWhenError) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowCustomMessageWhenError) } else { "—" } - $espCustomErrorMessage = if ($espConfig -and $espConfig.CustomErrorMessage) { [System.Net.WebUtility]::HtmlEncode($espConfig.CustomErrorMessage) } else { "—" } - $espAllowLog = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowLogCollectionOnFailure) } else { "—" } - $espTrackAutopilot = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.TrackAutopilotOnly) } else { "—" } - $espQuality = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.InstallQualityUpdates) } else { "—" } - $espBlockUntilAll = if ($espConfig -and $espConfig.ShowInstallationProgress) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowInstallationProgress) } else { "—" } - $espAllowReset = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowDeviceResetOnFailure) } else { "—" } - $espAllowUse = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowDeviceUseOnFailure) } else { "—" } - $espAllowNonBlock = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowNonBlockingAppInstallation) } else { "—" } + $espTimeout = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.InstallProgressTimeout) } else { "-" } + $espShowCustomMessage = if ($espConfig -and $espConfig.ShowCustomMessageWhenError) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowCustomMessageWhenError) } else { "-" } + $espCustomErrorMessage = if ($espConfig -and $espConfig.CustomErrorMessage) { [System.Net.WebUtility]::HtmlEncode($espConfig.CustomErrorMessage) } else { "-" } + $espAllowLog = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowLogCollectionOnFailure) } else { "-" } + $espTrackAutopilot = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.TrackAutopilotOnly) } else { "-" } + $espQuality = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.InstallQualityUpdates) } else { "-" } + $espBlockUntilAll = if ($espConfig -and $espConfig.ShowInstallationProgress) { [System.Net.WebUtility]::HtmlEncode($espConfig.ShowInstallationProgress) } else { "-" } + $espAllowReset = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowDeviceResetOnFailure) } else { "-" } + $espAllowUse = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowDeviceUseOnFailure) } else { "-" } + $espAllowNonBlock = if ($espConfig) { [System.Net.WebUtility]::HtmlEncode($espConfig.AllowNonBlockingAppInstallation) } else { "-" } $espRestHtml = "" if ($espShowProgress -ne "No") { $espBlockDeviceUseHtmlSafe = $espBlockDeviceUseHtml -replace '"', '`"' @@ -2448,12 +2448,12 @@ $espRestHtml $cloudPCPolicy = Get-CloudPCProvisioningConfigByDisplayName -DisplayName $cloudPCProvisioningMerged[0] if ($cloudPCPolicy) { $toYesNo = $script:ToYesNo - $cpName = if ($cloudPCPolicy.displayName) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.displayName) } else { "—" } + $cpName = if ($cloudPCPolicy.displayName) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.displayName) } else { "-" } $cpExperience = if ($cloudPCPolicy.userExperienceType -eq 'cloudPc') { "Access a full Cloud PC desktop" } else { [System.Net.WebUtility]::HtmlEncode([string]$cloudPCPolicy.userExperienceType) } $cpLicense = if ($cloudPCPolicy.managedBy -eq 'windows365') { "Enterprise" } else { [System.Net.WebUtility]::HtmlEncode([string]$cloudPCPolicy.managedBy) } $cpSso = & $toYesNo $cloudPCPolicy.enableSingleSignOn - $cpJoinType = "—" - $cpGeo = "—" + $cpJoinType = "-" + $cpGeo = "-" if ($cloudPCPolicy.domainJoinConfigurations -and @($cloudPCPolicy.domainJoinConfigurations).Count -gt 0) { $djc = @($cloudPCPolicy.domainJoinConfigurations)[0] $cpJoinType = if ($djc.type -eq 'azureADJoin') { "Microsoft Entra Join" } else { [System.Net.WebUtility]::HtmlEncode([string]$djc.type) } @@ -2467,13 +2467,13 @@ $espRestHtml $it = [string]$cloudPCPolicy.imageType $itEnc = if ($it.Length -gt 0) { $it.Substring(0, 1).ToUpper() + $it.Substring(1) } else { $it } [System.Net.WebUtility]::HtmlEncode($itEnc) - } else { "—" } - $cpImageName = if ($cloudPCPolicy.imageDisplayName) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.imageDisplayName) } else { "—" } - $cpLang = "—" + } else { "-" } + $cpImageName = if ($cloudPCPolicy.imageDisplayName) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.imageDisplayName) } else { "-" } + $cpLang = "-" if ($cloudPCPolicy.windowsSettings -and $cloudPCPolicy.windowsSettings.language) { $cpLang = [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.windowsSettings.language) } elseif ($cloudPCPolicy.windowsSetting -and $cloudPCPolicy.windowsSetting.locale) { $cpLang = [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.windowsSetting.locale) } $cpApplyNameTemplate = & $toYesNo ($cloudPCPolicy.cloudPcNamingTemplate -and [string]$cloudPCPolicy.cloudPcNamingTemplate.Trim().Length -gt 0) - $cpNameTemplate = if ($cloudPCPolicy.cloudPcNamingTemplate) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.cloudPcNamingTemplate) } else { "—" } + $cpNameTemplate = if ($cloudPCPolicy.cloudPcNamingTemplate) { [System.Net.WebUtility]::HtmlEncode($cloudPCPolicy.cloudPcNamingTemplate) } else { "-" } $cpScopeIds = if ($cloudPCPolicy.scopeIds -and @($cloudPCPolicy.scopeIds).Count -gt 0) { $scopeParts = $cloudPCPolicy.scopeIds | ForEach-Object { if ($_ -eq '0') { 'Default' } else { $_ } } [System.Net.WebUtility]::HtmlEncode(($scopeParts -join ', ')) @@ -2527,7 +2527,7 @@ $espRestHtml } else { $cloudPCProvisioningDeviceGroupsCount = 0 - $cloudPCProvisioningDeviceGroupsHtml = '
Policy name
Group name
Type
Membership rule
Policy name
— None —
Group name
Type
Membership rule
' + $cloudPCProvisioningDeviceGroupsHtml = '
Policy name
Group name
Type
Membership rule
Policy name
- None -
Group name
-
Type
-
Membership rule
-
' } } @@ -2536,15 +2536,15 @@ $espRestHtml if ($cloudPCUserSettingsMerged.Count -gt 0) { $cloudPCUserSettings = Get-CloudPCUserSettingsConfigByDisplayName -DisplayName $cloudPCUserSettingsMerged[0] if ($cloudPCUserSettings) { - $usName = if ($cloudPCUserSettings.Name) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.Name) } else { "—" } - $usSelfService = if ($cloudPCUserSettings.SelfServiceEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.SelfServiceEnabled) } else { "—" } - $usLocalAdmin = if ($cloudPCUserSettings.LocalAdminEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.LocalAdminEnabled) } else { "—" } - $usReset = if ($cloudPCUserSettings.ResetEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.ResetEnabled) } else { "—" } - $usRestoreFreq = if ($cloudPCUserSettings.RestorePointFrequency) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.RestorePointFrequency) } else { "—" } - $usUserRestore = if ($cloudPCUserSettings.UserRestoreEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.UserRestoreEnabled) } else { "—" } - $usDisasterRecovery = if ($cloudPCUserSettings.DisasterRecoveryEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.DisasterRecoveryEnabled) } else { "—" } - $usUserInitiatedDR = if ($cloudPCUserSettings.UserInitiatedDRAllowed) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.UserInitiatedDRAllowed) } else { "—" } - $usRestartPrompts = if ($cloudPCUserSettings.RestartPromptsDisabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.RestartPromptsDisabled) } else { "—" } + $usName = if ($cloudPCUserSettings.Name) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.Name) } else { "-" } + $usSelfService = if ($cloudPCUserSettings.SelfServiceEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.SelfServiceEnabled) } else { "-" } + $usLocalAdmin = if ($cloudPCUserSettings.LocalAdminEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.LocalAdminEnabled) } else { "-" } + $usReset = if ($cloudPCUserSettings.ResetEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.ResetEnabled) } else { "-" } + $usRestoreFreq = if ($cloudPCUserSettings.RestorePointFrequency) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.RestorePointFrequency) } else { "-" } + $usUserRestore = if ($cloudPCUserSettings.UserRestoreEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.UserRestoreEnabled) } else { "-" } + $usDisasterRecovery = if ($cloudPCUserSettings.DisasterRecoveryEnabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.DisasterRecoveryEnabled) } else { "-" } + $usUserInitiatedDR = if ($cloudPCUserSettings.UserInitiatedDRAllowed) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.UserInitiatedDRAllowed) } else { "-" } + $usRestartPrompts = if ($cloudPCUserSettings.RestartPromptsDisabled) { [System.Net.WebUtility]::HtmlEncode($cloudPCUserSettings.RestartPromptsDisabled) } else { "-" } $cloudPCUserSettingsConfigSection = @"
@@ -2593,7 +2593,7 @@ $espRestHtml } else { $cloudPCUserSettingsDeviceGroupsCount = 0 - $cloudPCUserSettingsDeviceGroupsHtml = '
Policy name
Group name
Type
Membership rule
Policy name
— None —
Group name
Type
Membership rule
' + $cloudPCUserSettingsDeviceGroupsHtml = '
Policy name
Group name
Type
Membership rule
Policy name
- None -
Group name
-
Type
-
Membership rule
-
' } $cloudPCDeviceGroupsCount = $cloudPCProvisioningDeviceGroupsCount $cloudPCDeviceGroupsHtml = $cloudPCProvisioningDeviceGroupsHtml @@ -2630,9 +2630,9 @@ $userTable $entraGroupTableHeader = '
Group name
Type
Membership rule
' if ($entraCount -gt 0) { foreach ($gd in $DeviceGroupDetails) { - $name = if ($gd.DisplayName) { $gd.DisplayName } else { "—" } + $name = if ($gd.DisplayName) { $gd.DisplayName } else { "-" } $gType = if ($gd.GroupType) { $gd.GroupType } else { "Static" } - $ruleHtml = if ($gd.MembershipRule) { $gd.MembershipRule } else { "—" } + $ruleHtml = if ($gd.MembershipRule) { $gd.MembershipRule } else { "-" } $encName = [System.Net.WebUtility]::HtmlEncode($name) $encType = [System.Net.WebUtility]::HtmlEncode($gType) $entraGroupListHtml += '
' @@ -2643,7 +2643,7 @@ $userTable } } else { - $entraGroupListHtml = '
Group name
— None —
Type
Membership rule
' + $entraGroupListHtml = '
Group name
- None -
Type
-
Membership rule
-
' } $entraGroupListHtml = $entraGroupTableHeader + $entraGroupListHtml $entraStepHtml = @" From 9cc1a549db1b6ebc7c41a7b25aee4932f487e062 Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:37:43 +0100 Subject: [PATCH 2/7] Update README and module files to require PowerShell 7.0 or higher, enhancing clarity on prerequisites and installation instructions. Adjusted PowerShellVersion in module manifest and added version check in module script. --- .github/ISSUE_TEMPLATE/bug_report.md | 17 +++++++++++++++-- README.md | 2 +- module/README.md | 2 ++ module/RKSolutions.psd1 | 2 +- module/RKSolutions.psm1 | 6 ++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 11a5325..18d1c4a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,9 +26,22 @@ What actually happened (error message, wrong output, or no output). ## Environment -- **PowerShell version:** (e.g. output of `$PSVersionTable.PSVersion`) +- **PowerShell version:** (output of `$PSVersionTable.PSVersion` — must be 7.0 or higher) +- **PowerShell host:** Are you running `pwsh` (PowerShell 7) or `powershell.exe` (Windows PowerShell 5.1)? - **OS:** (e.g. Windows 11, macOS 14, Ubuntu 22.04) -- **Module version:** (e.g. output of `Get-Module RKSolutions | Select-Object Version`) +- **Module version:** (output of `Get-Module RKSolutions | Select-Object Version`) + +## Installation & Import + +- **How did you install the module?** + - [ ] PowerShell Gallery (`Install-Module -Name RKSolutions`) + - [ ] GitHub (cloned/downloaded and `Import-Module ./module/RKSolutions.psd1`) + +- **Does the module load correctly?** + Run `Get-Command -Module RKSolutions` and paste the output: + ```powershell + # Paste output here + ``` ## Additional context diff --git a/README.md b/README.md index f149519..3b3007c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ RK-Solutions-PSModule/ ## Prerequisites -- **PowerShell 5.1+** or **PowerShell 7+** (cross-platform) +- **PowerShell 7.0 or higher** (Windows, macOS, Linux) - **Microsoft.Graph.Authentication** (and other Graph modules as required by the cmdlets) - **Microsoft Graph permissions:** App registration in Azure AD / Entra ID with the scopes required by the cmdlets you use. See **[Graph permissions (full list)](docs/PERMISSIONS.md)** for a per-cmdlet breakdown. diff --git a/module/README.md b/module/README.md index bfd14ff..9ffba3e 100644 --- a/module/README.md +++ b/module/README.md @@ -2,6 +2,8 @@ This is the script implementation of the RKSolutions module. It provides cmdlets to connect to Microsoft Graph and generate reports for Intune Enrollment Flows, Intune Anomalies, Entra Admin Roles, and M365 License Assignment. +> **Requires PowerShell 7.0 or higher.** Windows PowerShell 5.1 is not supported. + ## Module structure ``` diff --git a/module/RKSolutions.psd1 b/module/RKSolutions.psd1 index 947614e..52b60e5 100644 --- a/module/RKSolutions.psd1 +++ b/module/RKSolutions.psd1 @@ -6,7 +6,7 @@ CompanyName = 'RK Solutions' Copyright = '(c) 2026 Roy Klooster - RK Solutions. All rights reserved.' Description = 'PowerShell module consolidating Intune Enrollment Flows, Intune Anomalies, Entra Admin Roles, and M365 License Assignment reports. Connects to Microsoft Graph and generates HTML/CSV reports.' - PowerShellVersion = '5.1' + PowerShellVersion = '7.0' RequiredModules = @('Microsoft.Graph.Authentication') FunctionsToExport = @( 'Connect-RKGraph', diff --git a/module/RKSolutions.psm1 b/module/RKSolutions.psm1 index 46b1302..6420959 100644 --- a/module/RKSolutions.psm1 +++ b/module/RKSolutions.psm1 @@ -1,6 +1,12 @@ # RKSolutions.psm1 - Dot-source Private then Public; export only public functions # Private scripts (e.g. IntuneEnrollmentFlows.ps1) may define helper functions (e.g. Get-DeviceEvaluationContext, # Get-CloudPCProvisioningPolicyGroupInfo) that are used internally by Public cmdlets but are NOT exported as cmdlets. + +# Require PowerShell 7.0 or higher +if ($PSVersionTable.PSVersion.Major -lt 7) { + throw "RKSolutions module requires PowerShell 7.0 or higher. Current version: $($PSVersionTable.PSVersion). Please install PowerShell 7: https://aka.ms/powershell" +} + $moduleRoot = $PSScriptRoot # Load Private scripts first (shared helpers, then report-specific) From f6f48dde94cd77bb61cca925fae2c886e227567f Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:50:49 +0100 Subject: [PATCH 3/7] Enhance error messaging in RKSolutions module to clarify PowerShell version requirements and installation instructions. Update comments in IntuneAnomalies and IntuneEnrollmentFlows scripts for consistency in notation. --- module/Private/IntuneAnomalies.ps1 | 6 +++--- module/Private/IntuneEnrollmentFlows.ps1 | 4 ++-- module/RKSolutions.psm1 | 14 +++++++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/module/Private/IntuneAnomalies.ps1 b/module/Private/IntuneAnomalies.ps1 index 3de16ab..24e6bae 100644 --- a/module/Private/IntuneAnomalies.ps1 +++ b/module/Private/IntuneAnomalies.ps1 @@ -2240,13 +2240,13 @@ function Get-AllDeviceData { Write-Host "Fetching Autopilot devices..." -ForegroundColor Yellow $AutopilotDevices = (Invoke-GraphRequestWithPaging -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities") - # Pre-build Autopilot lookup hashtable (serialNumber → device object) for O(1) lookups + # Pre-build Autopilot lookup hashtable (serialNumber -> device object) for O(1) lookups $AutopilotLookup = @{} foreach ($ap in $AutopilotDevices) { if ($ap.serialNumber) { $AutopilotLookup[$ap.serialNumber] = $ap } } - # Pre-build user lookup hashtable (id → UPN) for O(1) lookups + # Pre-build user lookup hashtable (id -> UPN) for O(1) lookups $UserLookup = @{} foreach ($u in $AllEntraIDUsers) { if ($u.id -and $u.userPrincipalName) { $UserLookup[$u.id] = $u.userPrincipalName } @@ -2269,7 +2269,7 @@ function Get-AllDeviceData { Write-Progress -Activity "Processing Intune Devices" -Status "Processing device: $($DeviceData.DeviceName)" -CurrentOperation "$currentIndex of $totalDevices devices processed" -PercentComplete $progressPercent try { - # Use bulk-fetched data directly (no per-device re-fetch needed — same $select) + # Use bulk-fetched data directly (no per-device re-fetch needed - same $select) $DeviceProperties = $DeviceData # Process Autopilot information via pre-built hashtable diff --git a/module/Private/IntuneEnrollmentFlows.ps1 b/module/Private/IntuneEnrollmentFlows.ps1 index a85c80a..b8936d8 100644 --- a/module/Private/IntuneEnrollmentFlows.ps1 +++ b/module/Private/IntuneEnrollmentFlows.ps1 @@ -525,7 +525,7 @@ function Get-CloudPCPolicyWithAssignments { param([Parameter(Mandatory)][string]$EntityType, [Parameter(Mandatory)][string]$EntityId, [switch]$DebugMode) $baseUri = "https://graph.microsoft.com/beta/deviceManagement/$EntityType/$EntityId" $policy = $null - # User's approach: GET with expand (no -OutputType Json) → assignments may be Hashtable { groupId } = value, or PSCustomObject with GUID-named properties. + # User's approach: GET with expand (no -OutputType Json) -> assignments may be Hashtable { groupId } = value, or PSCustomObject with GUID-named properties. try { $policy = Invoke-MgGraphRequest -Uri ($baseUri + "?`$expand=assignments") -Method GET -ErrorAction Stop if (-not $policy) { } @@ -987,7 +987,7 @@ function Get-AllPolicyAssignmentsBatch { # Intents created from a template (templateId set) = Security Baseline; others = Endpoint Security $effectiveCategory = $policyType.Name if ($policyType.EntityType -eq "deviceManagement/intents" -and $policy.templateId) { $effectiveCategory = "Security Baselines" } - # Settings Catalog policies that are security baseline / version monitoring → show under Security Baselines in Overview + # Settings Catalog policies that are security baseline / version monitoring -> show under Security Baselines in Overview if ($policyType.EntityType -eq "configurationPolicies") { $nameForMatch = $policyName if (($nameForMatch -match 'Baseline' -and $nameForMatch -match 'Version Monitoring') -or ($nameForMatch -match 'Security Baseline')) { $effectiveCategory = "Security Baselines" } diff --git a/module/RKSolutions.psm1 b/module/RKSolutions.psm1 index 6420959..1a36487 100644 --- a/module/RKSolutions.psm1 +++ b/module/RKSolutions.psm1 @@ -4,7 +4,19 @@ # Require PowerShell 7.0 or higher if ($PSVersionTable.PSVersion.Major -lt 7) { - throw "RKSolutions module requires PowerShell 7.0 or higher. Current version: $($PSVersionTable.PSVersion). Please install PowerShell 7: https://aka.ms/powershell" + $msg = @" + +RKSolutions module requires PowerShell 7.0 or higher. +Current version: $($PSVersionTable.PSVersion) + +You are running Windows PowerShell (powershell.exe). +Please use PowerShell 7 (pwsh.exe) instead. + +Install PowerShell 7: https://aka.ms/powershell +After installing, run: pwsh + +"@ + throw $msg } $moduleRoot = $PSScriptRoot From b84c415efb4aa9a3e5754c72e19e9719e188cc56 Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:51:11 +0100 Subject: [PATCH 4/7] Update module version in RKSolutions.psd1 from 1.0.0 to 1.0.1 to reflect recent changes and improvements. --- module/RKSolutions.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/RKSolutions.psd1 b/module/RKSolutions.psd1 index 52b60e5..47cb8f9 100644 --- a/module/RKSolutions.psd1 +++ b/module/RKSolutions.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RKSolutions.psm1' - ModuleVersion = '1.0.0' + ModuleVersion = '1.0.1' GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' Author = 'Roy Klooster' CompanyName = 'RK Solutions' From 2199f044465a210088647f228ada897e843b0087 Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:53:33 +0100 Subject: [PATCH 5/7] Update PowerShellVersion in RKSolutions.psd1 from 7.0 to 5.1, change ProjectUri to GitHub, and revise ReleaseNotes to reflect version 1.0.1 changes, including improved error messaging and compatibility fixes. --- module/RKSolutions.psd1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/module/RKSolutions.psd1 b/module/RKSolutions.psd1 index 47cb8f9..44be347 100644 --- a/module/RKSolutions.psd1 +++ b/module/RKSolutions.psd1 @@ -6,7 +6,7 @@ CompanyName = 'RK Solutions' Copyright = '(c) 2026 Roy Klooster - RK Solutions. All rights reserved.' Description = 'PowerShell module consolidating Intune Enrollment Flows, Intune Anomalies, Entra Admin Roles, and M365 License Assignment reports. Connects to Microsoft Graph and generates HTML/CSV reports.' - PowerShellVersion = '7.0' + PowerShellVersion = '5.1' RequiredModules = @('Microsoft.Graph.Authentication') FunctionsToExport = @( 'Connect-RKGraph', @@ -23,8 +23,11 @@ PSData = @{ Tags = @('RKSolutions', 'Microsoft365', 'MicrosoftIntune', 'MicrosoftEntraID', 'MicrosoftGraph', 'DeviceManagement', 'Reporting') LicenseUri = 'https://opensource.org/licenses/MIT' - ProjectUri = 'https://www.powershellgallery.com' - ReleaseNotes = '1.0.0 - Initial module release. Consolidates Generate-IntuneEnrollmentFlowsReport, Generate-IntuneAnomaliesReport, Generate-EntraAdminRolesReport, and Generate-M365LicenseAssignmentReport scripts into a single module with shared auth, export, and email helpers.' + ProjectUri = 'https://github.com/royklo/RKSolutions-Module' + ReleaseNotes = @' +1.0.1 - Requires PowerShell 7.0 or higher. Fixed encoding issues for Windows compatibility. Improved error messaging when running on unsupported PowerShell versions. +1.0.0 - Initial module release. Consolidates Intune Enrollment Flows, Intune Anomalies, Entra Admin Roles, and M365 License Assignment reports into a single module. +'@ } } } From ef00d034883f519d5a541c3209dd669d5707173d Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:54:46 +0100 Subject: [PATCH 6/7] Refactor PowerShell version check in RKSolutions.psm1 to use Write-Host for improved user feedback. The error message now provides clearer instructions for installing PowerShell 7.0 or higher. --- module/RKSolutions.psm1 | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/module/RKSolutions.psm1 b/module/RKSolutions.psm1 index 1a36487..50b8bdb 100644 --- a/module/RKSolutions.psm1 +++ b/module/RKSolutions.psm1 @@ -4,19 +4,19 @@ # Require PowerShell 7.0 or higher if ($PSVersionTable.PSVersion.Major -lt 7) { - $msg = @" - -RKSolutions module requires PowerShell 7.0 or higher. -Current version: $($PSVersionTable.PSVersion) - -You are running Windows PowerShell (powershell.exe). -Please use PowerShell 7 (pwsh.exe) instead. - -Install PowerShell 7: https://aka.ms/powershell -After installing, run: pwsh - -"@ - throw $msg + Write-Host "" + Write-Host "RKSolutions module requires PowerShell 7.0 or higher." -ForegroundColor Red + Write-Host "Current version: $($PSVersionTable.PSVersion)" -ForegroundColor Red + Write-Host "" + Write-Host "You are running Windows PowerShell (powershell.exe)." -ForegroundColor Yellow + Write-Host "Please use PowerShell 7 (pwsh.exe) instead." -ForegroundColor Yellow + Write-Host "" + Write-Host "Install PowerShell 7: " -ForegroundColor White -NoNewline + Write-Host "https://aka.ms/powershell" -ForegroundColor Cyan + Write-Host "After installing, run: " -ForegroundColor White -NoNewline + Write-Host "pwsh" -ForegroundColor Green + Write-Host "" + throw "PowerShell 7.0 or higher is required." } $moduleRoot = $PSScriptRoot From c53fba37953c8d4a143d0d7af063da06fc5eca49 Mon Sep 17 00:00:00 2001 From: Roy Klooster Date: Wed, 11 Mar 2026 11:56:30 +0100 Subject: [PATCH 7/7] Change error handling in RKSolutions.psm1 to exit with status 1 instead of throwing an error for PowerShell version checks, improving script behavior on version mismatch. --- module/RKSolutions.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/RKSolutions.psm1 b/module/RKSolutions.psm1 index 50b8bdb..5057b84 100644 --- a/module/RKSolutions.psm1 +++ b/module/RKSolutions.psm1 @@ -16,7 +16,7 @@ if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Host "After installing, run: " -ForegroundColor White -NoNewline Write-Host "pwsh" -ForegroundColor Green Write-Host "" - throw "PowerShell 7.0 or higher is required." + exit 1 } $moduleRoot = $PSScriptRoot