common.ps1
|
#Requires -Version 5.0 function New-OperationResult { <# .SYNOPSIS Creates a structured result object for script and library operations. .DESCRIPTION Builds a small, consistent PSCustomObject that scripts can return when they need to report planned, skipped, completed, or failed work. The helper keeps the common fields in a predictable order while omitting optional fields that were not supplied, which lets each script keep a compact output shape. Target, Action, and Status are always present. Source, Scope, Detail, SkippedReason, and Error are included only when the matching parameter is supplied. Additional fields can be appended through -Property for script-specific metadata without creating another custom result helper. .PARAMETER Target Object targeted by the operation, such as a package name, registry value, file path, scheduled task, or capability name. .PARAMETER Source Optional subsystem or provider that handled the operation, such as Registry, FileSystem, WinGet, Win32Program, or UPFAppxPackage. .PARAMETER Scope Optional scope affected by the operation, such as CurrentUser, Machine, DefaultUser, or AllExistingUsers. .PARAMETER Action Operation performed or planned, such as Install, Uninstall, SetValue, RemoveShortcut, or Disable. .PARAMETER Status Result state, such as Completed, Removed, Skipped, Failed, DryRun, or an ExitCode:n value. .PARAMETER Detail Optional human-readable detail that explains the result. .PARAMETER SkippedReason Optional machine-readable reason when Status is Skipped. .PARAMETER ErrorMessage Optional failure detail. This is emitted as the Error property to avoid assigning to PowerShell's automatic $Error variable. .PARAMETER Property Optional hashtable of additional properties appended after the common fields. Existing common fields are not overwritten. .EXAMPLE PS> New-OperationResult -Target 'Microsoft.GetHelp' -Source 'UPFAppxPackage' -Action 'Uninstall' -Status 'Skipped' -SkippedReason 'NoMatch' Creates a package-style lifecycle result with Target, Source, Action, Status, and SkippedReason. .EXAMPLE PS> New-OperationResult -Target 'HKCU:\Console\%%Startup\DelegationTerminal' -Scope 'CurrentUser' -Action 'SetValue' -Status 'Completed' -Detail 'Terminal delegation default applied.' Creates a terminal configuration result with Target, Scope, Action, Status, and Detail. .EXAMPLE PS> New-OperationResult -Target 'OneDrive' -Action 'Uninstall' -Status 'Skipped' -Detail 'No OneDrive uninstaller was found.' Creates a compact script result that does not include Source or Scope columns because they were not supplied. .LINK https://github.com/adnoctem/winkit/lib/common.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Factory helper only creates a result object.')] [OutputType([PSCustomObject])] [CmdletBinding()] param ( [string]$Target, [string]$Source, [string]$Scope, [string]$Action, [string]$Status, [string]$Detail, [string]$SkippedReason, [string]$ErrorMessage, [hashtable]$Property ) $_result = [ordered]@{ Target = $Target } if ($PSBoundParameters.ContainsKey('Source')) { $_result.Source = $Source } if ($PSBoundParameters.ContainsKey('Scope')) { $_result.Scope = $Scope } $_result.Action = $Action $_result.Status = $Status if ($PSBoundParameters.ContainsKey('Detail')) { $_result.Detail = $Detail } if ($PSBoundParameters.ContainsKey('SkippedReason')) { $_result.SkippedReason = $SkippedReason } if ($PSBoundParameters.ContainsKey('ErrorMessage')) { $_result.Error = $ErrorMessage } if ($Property) { foreach ($_key in $Property.Keys) { if (-not $_result.Contains($_key)) { $_result[$_key] = $Property[$_key] } } } [PSCustomObject]$_result } function Add-OperationResult { <# .SYNOPSIS Adds a structured operation result to an existing result collection. .DESCRIPTION Creates an operation result with New-OperationResult and appends it to a mutable result collection, usually a System.Collections.ArrayList used by high-impact scripts that support -PassThru. By default the helper writes no pipeline output, matching the common pattern of accumulating results and returning them once at the end of a script. .PARAMETER Results Mutable result collection that receives the created result object. .PARAMETER Target Object targeted by the operation, such as a package name, registry value, file path, scheduled task, or capability name. .PARAMETER Source Optional subsystem or provider that handled the operation. .PARAMETER Scope Optional scope affected by the operation. .PARAMETER Action Operation performed or planned. .PARAMETER Status Result state. .PARAMETER Detail Optional human-readable detail that explains the result. .PARAMETER SkippedReason Optional machine-readable reason when Status is Skipped. .PARAMETER ErrorMessage Optional failure detail. This is emitted as the Error property. .PARAMETER Property Optional hashtable of additional properties appended after the common fields. .PARAMETER PassThru Return the created result object after adding it to the collection. .EXAMPLE PS> $results = New-Object System.Collections.ArrayList PS> Add-OperationResult -Results $results -Target 'MapsToastTask' -Source 'ScheduledTask' -Action 'Disable' -Status 'Disabled' -Detail 'Scheduled task disabled.' Adds a scheduled-task result to the collection without writing output. .EXAMPLE PS> Add-OperationResult -Results $results -Target 'Microsoft.WindowsTerminal' -Scope 'Machine' -Action 'Install' -Status 'Skipped' -Detail 'AlreadyInstalled' -PassThru Adds and returns the result object. .LINK https://github.com/adnoctem/winkit/lib/common.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.IList]$Results, [string]$Target, [string]$Source, [string]$Scope, [string]$Action, [string]$Status, [string]$Detail, [string]$SkippedReason, [string]$ErrorMessage, [hashtable]$Property, [switch]$PassThru ) $_resultParameters = @{ Target = $Target Action = $Action Status = $Status } foreach ($_parameterName in @('Source', 'Scope', 'Detail', 'SkippedReason', 'ErrorMessage', 'Property')) { if ($PSBoundParameters.ContainsKey($_parameterName)) { $_resultParameters[$_parameterName] = $PSBoundParameters[$_parameterName] } } $_result = New-OperationResult @_resultParameters [void]$Results.Add($_result) if ($PassThru) { $_result } } function Write-OperationResultLog { <# .SYNOPSIS Writes operation result objects to a JSON Lines log file. .DESCRIPTION Serializes each operation result as one compact JSON object per line in a temp-directory log file. The helper is intended for scripts that keep console output concise but still need an auditable record of every action, skipped action, and failure. Logs are written to %TEMP%\winkit\logs by default. Each line includes a timestamp, script name, and the properties already present on the result object, such as Target, Source, Scope, Action, Status, Detail, SkippedReason, or Error. .PARAMETER Results Operation result objects to serialize. .PARAMETER ScriptName Name used in the log entries and default file name. Defaults to the calling script name when available. .PARAMETER Path Optional explicit output file path. When omitted, a timestamped .jsonl file is created under %TEMP%\winkit\logs. .EXAMPLE PS> $path = Write-OperationResultLog -Results $results -ScriptName 'Remove-Bloatware' PS> Write-Log -Message "Operation log: $path" -Color Gray Writes the accumulated operation results and prints the resulting path. .LINK https://github.com/adnoctem/winkit/lib/common.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.IEnumerable]$Results, [string]$ScriptName, [string]$Path ) $_results = @($Results) if ($_results.Count -eq 0) { return $null } if ([string]::IsNullOrWhiteSpace($ScriptName)) { if ($MyInvocation.ScriptName) { $ScriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.ScriptName) } else { $ScriptName = 'winkit-operation' } } $_safeScriptName = $ScriptName -replace '[^A-Za-z0-9._-]', '-' if ([string]::IsNullOrWhiteSpace($Path)) { $_tempRoot = if ([string]::IsNullOrWhiteSpace($env:TEMP)) { [System.IO.Path]::GetTempPath() } else { $env:TEMP } $_logRoot = Join-Path -Path $_tempRoot -ChildPath 'winkit\logs' if (-not (Test-Path -LiteralPath $_logRoot)) { $null = [System.IO.Directory]::CreateDirectory($_logRoot) } $_timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff' $Path = Join-Path -Path $_logRoot -ChildPath "$_safeScriptName-$_timestamp.jsonl" } else { $_logRoot = Split-Path -Path $Path -Parent if (-not [string]::IsNullOrWhiteSpace($_logRoot) -and -not (Test-Path -LiteralPath $_logRoot)) { $null = [System.IO.Directory]::CreateDirectory($_logRoot) } } $_lines = foreach ($_result in $_results) { $_entry = [ordered]@{ Timestamp = (Get-Date).ToString('o') Script = $ScriptName } foreach ($_property in $_result.PSObject.Properties) { $_entry[$_property.Name] = $_property.Value } [PSCustomObject]$_entry | ConvertTo-Json -Compress -Depth 8 } $_encoding = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllLines($Path, [string[]]$_lines, $_encoding) return $Path } function Export-RegistrySettingState { <# .SYNOPSIS Exports registry setting definitions with Preferred set to current values. .DESCRIPTION Reads the current machine value for each registry setting object and returns a reusable configuration snapshot. The returned objects preserve the original setting metadata, including Path, Name, Default, Type, Group, Description, and build gates, but replace Preferred with the value found in the registry. Missing registry values are exported with Preferred = $null. This keeps the output compatible with existing -Config behavior while making absent values explicit for later diffs. .PARAMETER Settings Registry setting objects or hashtables with at least Path and Name properties. Additional properties are preserved. .EXAMPLE PS> Export-RegistrySettingState -Settings $taskbarSettings | ConvertTo-Json -Depth 3 Exports current taskbar registry values in the same schema consumed by Configure-Taskbar.ps1 -Config. .LINK https://github.com/adnoctem/winkit/lib/common.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Factory helper only creates result objects for registry setting definitions.')] [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]]$Settings ) process { foreach ($_setting in $Settings) { if ($null -eq $_setting) { continue } $_snapshot = [ordered]@{} if ($_setting -is [hashtable]) { foreach ($_key in $_setting.Keys) { $_snapshot[$_key] = $_setting[$_key] } } else { foreach ($_property in $_setting.PSObject.Properties) { $_snapshot[$_property.Name] = $_property.Value } } if (-not $_snapshot.Contains('Path') -or -not $_snapshot.Contains('Name')) { Write-Error 'Registry setting snapshots require Path and Name properties.' continue } $_currentValue = $null if (Test-RegistryValue -Path $_snapshot.Path -Name $_snapshot.Name) { $_currentValue = Get-RegistryValue -Path $_snapshot.Path -Name $_snapshot.Name $_currentKind = Get-RegistryValueKind -Path $_snapshot.Path -Name $_snapshot.Name if ($_currentKind -and $_snapshot.Contains('Type')) { $_snapshot.Type = $_currentKind.ToString() } } $_snapshot.Preferred = $_currentValue [PSCustomObject]$_snapshot } } } function ConvertTo-RegistrySettingResult { <# .SYNOPSIS Creates operation results for registry setting definitions. .DESCRIPTION Converts registry setting objects into operation-result records suitable for Write-OperationResultLog and -PassThru output. In DryRun mode the results describe planned SetValue or RemoveValue actions as Skipped with Detail = DryRun. In normal mode the helper reads the registry after the script has run and marks each setting Completed when the target state is present, Removed when an undo removal target is absent, or Failed when the current state does not match the expected state. This helper is intentionally conservative: it does not attempt to infer whether a Completed value was newly changed or already correct. Scripts that need exact lifecycle states can still add explicit results during their apply loop. .PARAMETER Settings Registry setting objects or hashtables with Path, Name, Preferred, Default, Type, and Description properties. .PARAMETER Undo Build results for an undo operation, using Default as the target state. .PARAMETER DryRun Build planned-operation results without reading final state. .PARAMETER Source Source label for result objects. Defaults to Registry. .EXAMPLE PS> $results = ConvertTo-RegistrySettingResult -Settings $taskbarSettings -DryRun PS> Write-OperationResultLog -Results $results -ScriptName 'Configure-Taskbar' Creates dry-run audit records for a registry-backed configuration script. .LINK https://github.com/adnoctem/winkit/lib/common.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]]$Settings, [switch]$Undo, [switch]$DryRun, [string]$Source = 'Registry' ) process { foreach ($_setting in $Settings) { if ($null -eq $_setting) { continue } $_path = $_setting.Path $_name = $_setting.Name $_target = "$_path\$_name" $_targetValue = if ($Undo) { $_setting.Default } else { $_setting.Preferred } $_action = if ($Undo -and $null -eq $_setting.Default) { 'RemoveValue' } else { 'SetValue' } $_detail = if ($_setting.PSObject.Properties.Name -contains 'Description') { $_setting.Description } else { $null } if ($DryRun) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Skipped' -Detail 'DryRun' continue } if ($_action -eq 'RemoveValue') { if ($_path -like 'Registry::HKEY_USERS\DefaultUser\*' -and -not (Test-Path -LiteralPath 'Registry::HKEY_USERS\DefaultUser')) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Skipped' -Detail 'DefaultUserHiveUnavailable' continue } if (Test-RegistryValue -Path $_path -Name $_name) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Failed' -Detail 'Value still exists after undo.' } else { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Removed' -Detail $_detail } continue } if ($_path -like 'Registry::HKEY_USERS\DefaultUser\*' -and -not (Test-Path -LiteralPath 'Registry::HKEY_USERS\DefaultUser')) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Skipped' -Detail 'DefaultUserHiveUnavailable' continue } if (-not (Test-RegistryValue -Path $_path -Name $_name)) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Failed' -Detail 'Value is missing after apply.' continue } $_currentValue = Get-RegistryValue -Path $_path -Name $_name $_matches = $false if ($null -eq $_currentValue -and $null -eq $_targetValue) { $_matches = $true } elseif ($null -ne $_currentValue -and $null -ne $_targetValue) { if ($_currentValue -is [array] -or $_targetValue -is [array]) { $_matches = (($_currentValue -join "`0") -eq ($_targetValue -join "`0")) } else { $_matches = ($_currentValue.ToString() -eq $_targetValue.ToString()) } } if ($_matches) { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Completed' -Detail $_detail } else { New-OperationResult -Target $_target -Source $Source -Action $_action -Status 'Failed' -Detail "Expected '$($_targetValue)' but found '$($_currentValue)'." } } } } |