public/Compare-PSProfileSnapshot.ps1

<#
    .SYNOPSIS
        Compares two snapshots or a snapshot with the current profile.
    .DESCRIPTION
        Shows the differences between two snapshots, or between a snapshot and the
        current profile state. Differences are displayed in a clear side-by-side format.
    .PARAMETER Name1
        The name of the first snapshot (or current profile if this is the only parameter).
        Can be piped from Get-PSProfileSnapshot.
    .PARAMETER Name2
        The name of the second snapshot to compare against the first. If not specified,
        the current profile is used as the comparison target.
    .PARAMETER GUID1
        The GUID of the first snapshot (alternative to Name1).
        Can be piped from Get-PSProfileSnapshot.
    .PARAMETER GUID2
        The GUID of the second snapshot (alternative to Name2).
    .INPUTS
        None
    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns difference objects with properties: InputObject, SideIndicator
    .EXAMPLE
        Compare-PSProfileSnapshot -Name1 "before-change" -Name2 "after-change"

        Shows differences between two named snapshots.
    .EXAMPLE
        Compare-PSProfileSnapshot -Name1 "old-config"

        Shows differences between the "old-config" snapshot and the current profile.
    .EXAMPLE
        Get-PSProfileSnapshot | Where-Object Name -EQ "my-snapshot" | Compare-PSProfileSnapshot

        Shows differences by piping snapshot from Get-PSProfileSnapshot.
    .NOTES
        Use the SideIndicator property to identify which profile each line comes from:
        - "=>" : Line exists in the second/current profile
        - "<=" : Line exists only in the first snapshot
        - "==" : Line exists in both (for reference)
#>


function Compare-PSProfileSnapshot {
    [CmdletBinding(DefaultParameterSetName = 'CompareWithCurrent')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByName1', ValueFromPipelineByPropertyName = $true)]
        [Alias('SnapshotName', 'SnapshotName1')]
        [string]$Name1,

        [Parameter(Mandatory = $false, ParameterSetName = 'ByNameBoth')]
        [Alias('SnapshotName2')]
        [string]$Name2,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByGUID1', ValueFromPipelineByPropertyName = $true)]
        [Alias('SnapshotGUID', 'SnapshotGUID1')]
        [guid]$GUID1,

        [Parameter(Mandatory = $false, ParameterSetName = 'ByGUIDBoth')]
        [Alias('SnapshotGUID2')]
        [guid]$GUID2
    )

    try {
        $profilesPath = Split-Path -Path $profile -Parent
        $backupFiles = Get-ChildItem -Path $profilesPath -Filter "*.backup" -ErrorAction Stop

        # Helper function to find a snapshot by name or GUID
        function Get-SnapshotContent {
            param([string]$Name, [guid]$GUID)

            foreach ($backupFile in $backupFiles) {
                $fileGUID = $backupFile.BaseName -replace '^.*_([a-f0-9\-]+)$', '$1'
                
                if ($GUID) {
                    if ($fileGUID -eq $GUID.ToString()) {
                        return Get-Content -Path $backupFile.FullName -Raw -ErrorAction Stop
                    }
                }
                elseif ($Name) {
                    # Check metadata first for custom name
                    $metadataFileName = "$($profile)_$($fileGUID).metadata"
                    if (Test-Path -Path $metadataFileName -ErrorAction SilentlyContinue) {
                        $metadata = Get-Content -Path $metadataFileName -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue
                        if ($metadata.Name -eq $Name) {
                            return Get-Content -Path $backupFile.FullName -Raw -ErrorAction Stop
                        }
                    }
                    
                    # Also check if name matches the timestamp format of CreatedTime
                    $timestampName = $backupFile.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
                    if ($timestampName -eq $Name) {
                        return Get-Content -Path $backupFile.FullName -Raw -ErrorAction Stop
                    }
                }
            }
            
            return $null
        }

        # Determine which parameter set was used
        $referenceContent = $null
        $comparisonContent = $null
        $referenceLabel = ""
        $comparisonLabel = ""

        switch ($PSCmdlet.ParameterSetName) {
            'ByName1' {
                $referenceContent = Get-SnapshotContent -Name $Name1
                $referenceLabel = "Snapshot: $Name1"
                $comparisonContent = Get-Content -Path $profile -Raw -ErrorAction Stop
                $comparisonLabel = "Current Profile"
            }

            'ByNameBoth' {
                $referenceContent = Get-SnapshotContent -Name $Name1
                $referenceLabel = "Snapshot: $Name1"
                $comparisonContent = Get-SnapshotContent -Name $Name2
                $comparisonLabel = "Snapshot: $Name2"
            }

            'ByGUID1' {
                $referenceContent = Get-SnapshotContent -GUID $GUID1
                $referenceLabel = "Snapshot: $GUID1"
                $comparisonContent = Get-Content -Path $profile -Raw -ErrorAction Stop
                $comparisonLabel = "Current Profile"
            }

            'ByGUIDBoth' {
                $referenceContent = Get-SnapshotContent -GUID $GUID1
                $referenceLabel = "Snapshot: $GUID1"
                $comparisonContent = Get-SnapshotContent -GUID $GUID2
                $comparisonLabel = "Snapshot: $GUID2"
            }
        }

        # Validate that we found the snapshots
        if (-not $referenceContent) {
            Write-Error "❌ Could not find first snapshot."
            return
        }

        if (-not $comparisonContent) {
            Write-Error "❌ Could not find second snapshot or current profile."
            return
        }

        # Split content into lines for comparison
        $referenceSplit = $referenceContent -split "`n"
        $comparisonSplit = $comparisonContent -split "`n"

        # Perform the comparison
        Write-Host "`n----- Comparison: $referenceLabel vs $comparisonLabel -----`n" -ForegroundColor Cyan
        
        $differences = Compare-Object -ReferenceObject $referenceSplit -DifferenceObject $comparisonSplit -IncludeEqual | 
            Where-Object { $_.SideIndicator -ne '==' }

        if ($differences.Count -eq 0) {
            Write-Host "✅ No differences found between the profiles." -ForegroundColor Green
        }
        else {
            Write-Host "Found $($differences.Count) difference(s):`n" -ForegroundColor Yellow
            
            foreach ($diff in $differences) {
                $sideIndicator = $diff.SideIndicator
                if ($sideIndicator -eq '<=') {
                    Write-Host "← Only in $referenceLabel" -ForegroundColor Red
                }
                else {
                    Write-Host "→ Only in $comparisonLabel" -ForegroundColor Green
                }
                Write-Host " $($diff.InputObject)" -ForegroundColor Gray
                Write-Host ""
            }
        }

        return $differences
    }
    catch {
        Write-Error "❌ An error occurred while comparing snapshots."
        throw $_.Exception.Message
    }
}