profileupdater.psm1
#!/usr/bin/env pwsh #region Classes # Main class class ProfileUpdater { [string]$GitHubUsername = 'chadnpc' [string]$GistFileName = 'Microsoft.PowerShell_profile.ps1' [int]$BackupCount = 3 [string]$Token ProfileUpdater() {} [void] Update([string]$GistId) { $this.Update($GistId, $false, $false) } [void] Update([string]$GistId, [bool]$Force, [bool]$Preview) { # Initialize variables $script:Headers = @{ 'User-Agent' = 'PowerShell-Profile-Updater/1.0' 'Accept' = 'application/vnd.github.v3+json' } # Add authentication if token is provided if ($this.Token) { $script:Headers['Authorization'] = "token $($this.Token)" } Write-Host "🔄 PowerShell Profile Updater" -ForegroundColor Cyan Write-Host "================================" -ForegroundColor Cyan $P = Get-Variable -ValueOnly PROFILE try { # Ensure profile directory exists $profileDir = Split-Path $P -Parent if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Path $profileDir -Force | Out-Null Write-Host "✅ Created profile directory: $profileDir" -ForegroundColor Green } # Create profile file if it doesn't exist if (-not (Test-Path $P)) { New-Item -ItemType File -Path $P -Force | Out-Null Write-Host "✅ Created profile file: $P" -ForegroundColor Green } # Get current profile content and version $currentContent = Get-Content $P -Raw -ErrorAction SilentlyContinue $currentVersion = $this.GetProfileVersion($currentContent) Write-Host "📍 Current profile version: $currentVersion" -ForegroundColor Yellow # Get gist content $gistContent = $this.GetGistContent($this.GitHubUsername, $GistId, $this.GistFileName) if (-not $gistContent) { throw "Failed to retrieve gist content" } # Get remote version $remoteVersion = $this.GetProfileVersion($gistContent) Write-Host "🌐 Remote profile version: $remoteVersion" -ForegroundColor Yellow # Compare versions and content $shouldUpdate = $Force -or ($this.CompareVersions($currentVersion, $remoteVersion)) -or ($this.CompareContent($currentContent, $gistContent)) if (-not $shouldUpdate) { Write-Host "✅ Profile is already up to date!" -ForegroundColor Green return } # Preview mode if ($Preview) { $this.ShowChangesPreview($currentContent, $gistContent) return } # Confirm update unless forced if (-not $Force) { $confirmation = Read-Host "🤔 Update profile from version $currentVersion to $remoteVersion ? (Y/n)" if ($confirmation -eq 'n' -or $confirmation -eq 'N') { Write-Host "❌ Update cancelled by user" -ForegroundColor Yellow return } } # Create backup $this.BackupProfile($this.BackupCount) # Update profile Set-Content -Path $P -Value $gistContent -Encoding UTF8 Write-Host "✅ Profile updated successfully!" -ForegroundColor Green # Offer to reload profile $reload = Read-Host "🔄 Reload profile now? (Y/n)" if ($reload -ne 'n' -and $reload -ne 'N') { . $P Write-Host "✅ Profile reloaded!" -ForegroundColor Green } } catch { Write-Host "❌ Error updating profile: $($_.Exception.Message)" -ForegroundColor Red # Attempt to restore from backup if update failed $latestBackup = Get-ChildItem (Split-Path $P -Parent) -Filter "*.backup*" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($latestBackup) { $restore = Read-Host "🔄 Restore from latest backup? (Y/n)" if ($restore -ne 'n' -and $restore -ne 'N') { Copy-Item $latestBackup.FullName $P -Force Write-Host "✅ Profile restored from backup" -ForegroundColor Green } } } } [void] RunTests() { $this.RunTests($true) } [void] RunTests([bool]$interactive) { if (!$interactive) { $this.TestUpdate(); return } Write-Host "🚀 PowerShell Profile Updater Test Suite" -ForegroundColor Cyan Write-Host "=========================================" -ForegroundColor Cyan Write-Host "" Write-Host "Choose an option:" -ForegroundColor Yellow Write-Host "1. Run tests" -ForegroundColor Green Write-Host "2. Show usage examples" -ForegroundColor Green Write-Host "3. Exit" -ForegroundColor Green $choice = Read-Host "`nEnter your choice (1-3)" switch ($choice) { "1" { $this.TestUpdate() } "2" { $this.ShowUsage() } "3" { Write-Host "👋 Goodbye!" -ForegroundColor Cyan } default { Write-Host "❌ Invalid choice" -ForegroundColor Red } } } [void] ShowUsage () { <# .SYNOPSIS Shows usage examples for the PowerShell Profile Updater #> Write-Host "📖 PowerShell Profile Updater - Usage Examples" -ForegroundColor Cyan Write-Host "===============================================" -ForegroundColor Cyan $examples = @( @{ Title = "Basic Update (Public Gist)" Command = "Update-PowerShellProfile" Description = "Updates from default user 'chadnpc' public gist" }, @{ Title = "Preview Changes" Command = "Update-PowerShellProfile -Preview" Description = "Shows what would change without applying updates" }, @{ Title = "Force Update" Command = "Update-PowerShellProfile -Force" Description = "Updates without confirmation prompts" }, @{ Title = "Specific User" Command = "Update-PowerShellProfile -GitHubUsername 'yourusername'" Description = "Updates from a specific GitHub user's gist" }, @{ Title = "Specific Gist ID" Command = "Update-PowerShellProfile -GistId 'abc123def456'" Description = "Updates from a specific gist by ID" }, @{ Title = "With GitHub Token" Command = "Update-PowerShellProfile -Token 'ghp_xxxxxxxxxxxx'" Description = "Uses GitHub token for private gists or rate limit avoidance" }, @{ Title = "Custom Settings" Command = "Update-PowerShellProfile -GistFileName 'profile.ps1' -BackupCount 5" Description = "Custom filename and backup retention settings" } ) foreach ($example in $examples) { Write-Host "`n🔹 $($example.Title)" -ForegroundColor Yellow Write-Host " $($example.Command)" -ForegroundColor Green Write-Host " $($example.Description)" -ForegroundColor Gray } Write-Host "`n📋 Available Parameters:" -ForegroundColor Cyan Write-Host " -GitHubUsername : GitHub username (default: 'chadnpc')" -ForegroundColor Gray Write-Host " -GistId : Specific gist ID" -ForegroundColor Gray Write-Host " -GistFileName : Profile filename in gist" -ForegroundColor Gray Write-Host " -Force : Skip confirmations" -ForegroundColor Gray Write-Host " -Preview : Show changes without applying" -ForegroundColor Gray Write-Host " -BackupCount : Number of backups to keep (default: 3)" -ForegroundColor Gray Write-Host " -Token : GitHub personal access token" -ForegroundColor Gray } [void] TestUpdate() { <# .SYNOPSIS Tests the PowerShell Profile Updater functionality .DESCRIPTION Runs various tests to ensure the profile updater works correctly #> Write-Host "🧪 Testing PowerShell Profile Updater" -ForegroundColor Cyan Write-Host "=====================================" -ForegroundColor Cyan # Test 1: Check if function is loaded Write-Host "`n[+] 1. Testing function availability..." -ForegroundColor Yellow if (Get-Command Update-PowerShellProfile -ErrorAction SilentlyContinue) { Write-Host "✅ Update-PowerShellProfile function is available" -ForegroundColor Green } else { Write-Host "❌ Update-PowerShellProfile function not found" -ForegroundColor Red return } # Test 2: Test helper functions Write-Host "`n[+] 2. Testing helper functions..." -ForegroundColor Yellow # Test version parsing $testContent = "`n# Version 1.2.3`n# This is a test profile`nWrite-Host 'Hello World'" $version = $this.GetProfileVersion($testContent) if ($version -eq "1.2.3") { Write-Host "✅ Version parsing works correctly" -ForegroundColor Green } else { Write-Host "❌ Version parsing failed. Expected '1.2.3', got '$version'" -ForegroundColor Red } # Test version comparison $isNewer = $this.CompareVersions("1.0.0", "1.1.0") if ($isNewer) { Write-Host "✅ Version comparison works correctly" -ForegroundColor Green } else { Write-Host "❌ Version comparison failed" -ForegroundColor Red } # Test 3: Test with preview mode (safe test) Write-Host "`n[+] 3. Testing preview mode..." -ForegroundColor Yellow try { Update-PowerShellProfile -Preview -GitHubUsername "chadnpc" -ErrorAction Stop Write-Host "✅ Preview mode executed successfully" -ForegroundColor Green } catch { Write-Host "⚠️ Preview mode test failed: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host " This might be due to network issues or gist not found" -ForegroundColor Gray } # Test 4: Check backup functionality Write-Host "`n[+] 4. Testing backup functionality..." -ForegroundColor Yellow if (Test-Path $(Get-Variable -ValueOnly PROFILE)) { try { $this.BackupProfile(1) $profileDir = Split-Path (Get-Variable -ValueOnly PROFILE) -Parent $profileName = Split-Path (Get-Variable -ValueOnly PROFILE) -Leaf $backups = Get-ChildItem $profileDir -Filter "$profileName.backup.*" if ($backups.Count -gt 0) { Write-Host "✅ Backup creation works correctly" -ForegroundColor Green # Clean up test backup $backups | Remove-Item -Force Write-Host "🧹 Cleaned up test backup" -ForegroundColor Gray } else { Write-Host "❌ Backup creation failed" -ForegroundColor Red } } catch { Write-Host "❌ Backup test failed: $($_.Exception.Message)" -ForegroundColor Red } } else { Write-Host "⚠️ No existing profile found to test backup functionality" -ForegroundColor Yellow } Write-Host "`n🎉 Testing completed!" -ForegroundColor Cyan Write-Host "`n💡 To test with your actual gist, run:" -ForegroundColor Blue Write-Host " Update-PowerShellProfile -Preview" -ForegroundColor Gray Write-Host "`n💡 To perform an actual update, run:" -ForegroundColor Blue Write-Host " Update-PowerShellProfile" -ForegroundColor Gray } # helper methods: [version] GetProfileVersion([string]$Content) { if ([string]::IsNullOrEmpty($Content)) { return "0.0.0" } if ($Content -match '# Version (?<Version>\d+\.\d+\.\d+)') { return $matches.Version } # Fallback to last modified date if no version found if ($Content -match '# Last Modified: (?<LastModified>\d{4}-\d{2}-\d{2})') { return "1.0.0" # Assume version 1.0.0 if only date is found } return "0.0.0" } [bool] CompareVersions([string]$Current, [string]$Remote) { try { $currentVer = [version]$Current $remoteVer = [version]$Remote return $remoteVer -gt $currentVer } catch { # If version comparison fails, assume update is needed return $true } } [bool] CompareContent([string]$Current, [string]$Remote) { if ([string]::IsNullOrEmpty($Current) -and -not [string]::IsNullOrEmpty($Remote)) { return $true } # Simple hash comparison $currentHash = (Get-FileHash -InputStream ([System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($Current)))).Hash $remoteHash = (Get-FileHash -InputStream ([System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($Remote)))).Hash return $currentHash -ne $remoteHash } [void] ShowChangesPreview([string]$Current, [string]$Remote) { Write-Host "`n📋 Profile Update Preview" -ForegroundColor Cyan Write-Host "=========================" -ForegroundColor Cyan if ([string]::IsNullOrEmpty($Current)) { Write-Host "Current profile is empty or doesn't exist" -ForegroundColor Yellow Write-Host "`nNew profile will contain:" -ForegroundColor Green Write-Host $Remote.Substring(0, [Math]::Min(500, $Remote.Length)) -ForegroundColor Gray if ($Remote.Length -gt 500) { Write-Host "... (truncated)" -ForegroundColor Gray } } else { # Simple diff-like comparison $currentLines = $Current -split "`n" $remoteLines = $Remote -split "`n" Write-Host "Lines in current profile: $($currentLines.Count)" -ForegroundColor Yellow Write-Host "Lines in remote profile: $($remoteLines.Count)" -ForegroundColor Yellow # Show first few lines of each for comparison Write-Host "`nFirst 10 lines of current profile:" -ForegroundColor Cyan $currentLines | Select-Object -First 10 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } Write-Host "`nFirst 10 lines of remote profile:" -ForegroundColor Green $remoteLines | Select-Object -First 10 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } } } [void] BackupProfile() { $this.BackupProfile(3) } [void] BackupProfile([int]$BackupCount) { $P = $(Get-Variable -ValueOnly PROFILE) if (-not (Test-Path $P)) { return } $profileDir = Split-Path $P -Parent $profileName = Split-Path $P -Leaf $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $backupPath = Join-Path $profileDir "$profileName.backup.$timestamp" Copy-Item $P $backupPath -Force Write-Host "📁 Created backup: $backupPath" -ForegroundColor Green # Clean up old backups $backups = Get-ChildItem $profileDir -Filter "$profileName.backup.*" | Sort-Object LastWriteTime -Descending if ($backups.Count -gt $BackupCount) { $backups | Select-Object -Skip $BackupCount | Remove-Item -Force Write-Host "🧹 Cleaned up old backups (keeping $BackupCount)" -ForegroundColor Gray } } [string] GetGistContent([string]$GitHubUsername, [string]$GistId, [string]$GistFileName) { $gistUrl = '' try { # If specific gist ID is provided, use it directly if ($GistId) { $gistUrl = "https://api.github.com/gists/$GistId" Write-Host "🔍 Fetching gist by ID: $GistId" -ForegroundColor Yellow } else { # Search for gists by username Write-Host "🔍 Searching gists for user: $GitHubUsername" -ForegroundColor Yellow $gistsUrl = "https://api.github.com/users/$GitHubUsername/gists" # Try public gists first try { $gists = Invoke-RestMethod -Uri $gistsUrl -Headers $script:Headers -ErrorAction Stop -Verbose:$false # Find gist containing the profile file $targetGist = $gists | Where-Object { $_.files.PSObject.Properties.Name -contains $GistFileName } | Select-Object -First 1 if (-not $targetGist) { throw "No gist found containing file: $GistFileName" } $gistUrl = $targetGist.url Write-Host "✅ Found gist: $($targetGist.id)" -ForegroundColor Green } catch { # If public search fails, prompt for authentication Write-Host "⚠️ Public gist search failed. Gist may be private." -ForegroundColor Yellow if (-not $script:Headers.ContainsKey('Authorization')) { $this.Token = Read-Host "Enter GitHub token for private gist access (or press Enter to skip)" -AsSecureString if ($this.Token.Length -gt 0) { $tokenPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($this.Token)) $script:Headers['Authorization'] = "token $tokenPlain" # Retry with authentication $gists = Invoke-RestMethod -Uri $gistsUrl -Headers $script:Headers -ErrorAction Stop -Verbose:$false $targetGist = $gists | Where-Object { $_.files.PSObject.Properties.Name -contains $GistFileName } | Select-Object -First 1 if (-not $targetGist) { throw "No gist found containing file: $GistFileName" } $gistUrl = $targetGist.url } else { throw "Authentication required for private gist access" } } } } # Get the specific gist $gist = Invoke-RestMethod -Uri $gistUrl -Headers $script:Headers -ErrorAction Stop -Verbose:$false # Find the profile file in the gist $profileFile = $gist.files.PSObject.Properties | Where-Object { $_.Name -eq $GistFileName -or $_.Name -like "*profile*" } | Select-Object -First 1 if (-not $profileFile) { throw "Profile file '$GistFileName' not found in gist" } Write-Host "📥 Downloading: $($profileFile.Name)" -ForegroundColor Green # Get the raw content $rawUrl = $profileFile.Value.raw_url $content = Invoke-RestMethod -Uri $rawUrl -Headers $script:Headers -ErrorAction Stop -Verbose:$false return $content } catch { Write-Error "Failed to get gist content: $($_.Exception.Message)" return $null } } } #endregion Classes # Types that will be available to users when they import the module. $typestoExport = @( [ProfileUpdater] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') foreach ($Type in $typestoExport) { if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists.' ) -join ' - ' "TypeAcceleratorAlreadyExists $Message" | Write-Debug } } # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |