winpkg.ps1

<#PSScriptInfo
 
.VERSION 1.6
 
.GUID 30675ad6-2459-427d-ac3a-3304cf103fe9
 
.AUTHOR ni.guerra@proton.me
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI https://github.com/EnneGiKVis96/winpkg
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 Better Winget CLI with Powershell 7 Official Modules
 
#>
 
param(  
    [Parameter(ParameterSetName='00')]
    [switch]$Update,  
    [Parameter(ParameterSetName='01')]
    [string]$Install,
    [string]$Version,
    [Parameter(ParameterSetName='03')]
    [string]$Find,
    [Parameter(ParameterSetName='04')]
    [string]$Exclude,
    [Parameter(ParameterSetName='05')]
    [switch]$List,
    [Parameter(ParameterSetName='06')]
    [string]$Remove,
    [Parameter(ParameterSetName='07')]
    [switch]$Help,
    [Parameter(ParameterSetName='08')]
    [string]$Process,
    [Parameter(ParameterSetName='09')]
    [switch]$XclusionList
)

#Requires -Module Microsoft.WinGet.Client

# Constants
$SCRIPT_VERSION = "1.6"
$JSON_FILE_PATH = Join-Path $PSScriptRoot "exclusions.json"

# Visual Elements
$SEPARATOR = "═" * 80
$SUCCESS_ICON = "✓"
$ERROR_ICON = "✗"
$INFO_ICON = "ℹ"
$WARNING_ICON = "⚠"
$PROGRESS_ICON = "⟳"

# Title with Unicode styling
$TITLE = "╔══════════════════════════════════════════════════════════════════════════════╗`n" + 
        "║ ║`n" +
        "║ ██╗ ██╗██╗███╗ ██╗██████╗ ██╗ ██╗ ██████╗ ║`n" +
        "║ ██║ ██║██║████╗ ██║██╔══██╗ ██║ ██╔╝██╔════╝ ║`n" +
        "║ ██║ █╗ ██║██║██╔██╗ ██║██████╔╝ █████╔╝ ██║ ║`n" +
        "║ ██║███╗██║██║██║╚██╗██║██╔═══╝ ██╔═██╗ ██║ ██ [$SCRIPT_VERSION] ║`n" +
        "║ ╚███╔███╔╝██║██║ ╚████║██║ ██║ ██╗╚██████╗ ║`n" +
        "║ ╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ║`n" +
        "║ ║`n" +
        "╚══════════════════════════════════════════════════════════════════════════════╝"

# Utility functions
function Write-Status {
    param(
        [string]$Message,
        [string]$Color = "Blue",
        [string]$Icon = $INFO_ICON,
        [switch]$NoNewLine
    )
    $timestamp = Get-Date -Format "HH:mm:ss"
    $formattedMessage = "[$timestamp] $Icon $Message"
    if ($NoNewLine) {
        Write-Host $formattedMessage -ForegroundColor $Color -NoNewline
    } else {
        Write-Host $formattedMessage -ForegroundColor $Color
    }
}

function Write-Header {
    param([string]$Title)
    Write-Host "`n$SEPARATOR"
    Write-Host " $Title" -ForegroundColor Cyan
    Write-Host $SEPARATOR
}

function Write-Success {
    param([string]$Message)
    Write-Status -Message $Message -Color "Green" -Icon $SUCCESS_ICON
}

function Write-Error {
    param([string]$Message)
    Write-Status -Message $Message -Color "Red" -Icon $ERROR_ICON
}

function Write-Warning {
    param([string]$Message)
    Write-Status -Message $Message -Color "Yellow" -Icon $WARNING_ICON
}

function Write-Progress {
    param(
        [string]$Message,
        [int]$Percent
    )
    $progressBar = "[" + ("█" * ($Percent / 2)) + (" " * (50 - ($Percent / 2))) + "]"
    $progressText = "[$((Get-Date).ToString('HH:mm:ss'))] $PROGRESS_ICON $Message $progressBar $Percent%"
    Write-Host "`r$progressText" -ForegroundColor Cyan -NoNewline
    if ($Percent -eq 100) {
        Write-Host ""  # Aggiungi una nuova riga solo quando raggiungiamo il 100%
    }
}

function Format-PackageOutput {
    param(
        [int]$Index,
        [string]$Id,
        [string]$Version,
        [string]$NewVersion = $null
    )
    $formattedInd = "{0,-5}" -f $Index
    $formattedId = "{0,-40}" -f $Id
    $formattedVersion = "{0,-20}" -f $Version
    
    Write-Host -NoNewLine $formattedInd -ForegroundColor Magenta
    Write-Host -NoNewLine $formattedId
    Write-Host -NoNewLine $formattedVersion -ForegroundColor Red
    
    if ($NewVersion) {
        Write-Host $NewVersion -ForegroundColor Green
    } else {
        Write-Host ""
    }
}

function Get-WinPKGUpdates {
    $jsonContent = @{
        ExcludedPackages = @()
        LastUpdateCheck = $null
    }
    
    if (Test-Path $JSON_FILE_PATH) {
        try {
            $existingContent = Get-Content -Raw -Path $JSON_FILE_PATH | ConvertFrom-Json
            if ($existingContent.ExcludedPackages) {
                $jsonContent.ExcludedPackages = $existingContent.ExcludedPackages
            }
            if ($existingContent.LastUpdateCheck) {
                $jsonContent.LastUpdateCheck = $existingContent.LastUpdateCheck
            }
        } catch {
            Write-Status "Failed to read JSON file: $_" "Red"
        }
    }

    $shouldCheck = $true
    if ($jsonContent.LastUpdateCheck) {
        try {
            $lastCheck = [DateTime]::Parse($jsonContent.LastUpdateCheck, [System.Globalization.CultureInfo]::CurrentCulture)
            $today = Get-Date
            if (($today - $lastCheck).TotalDays -lt 1) {
                $shouldCheck = $false
            }
        } catch {
            Write-Status "Failed to parse last update check time: $_" "Red"
            $shouldCheck = $true
        }
    }

    if (-not $shouldCheck) {
        return
    }

    Write-Status "Checking for WinPKG updates" "Cyan"
    try {
        $galleryScript = Find-Script -Name "winpkg" -ErrorAction Stop
        if ($galleryScript.Version -gt $SCRIPT_VERSION) {
            Write-Status "New version of WinPKG is available: $($galleryScript.Version). Please update with Update-Script -Name winpkg" "Yellow"
            exit
        } else {
            Write-Status "Already running the latest version" "Cyan"
        }
        $jsonContent.LastUpdateCheck = (Get-Date).ToString("G", [System.Globalization.CultureInfo]::CurrentCulture)
        $jsonContent | ConvertTo-Json | Set-Content -Path $JSON_FILE_PATH
    } catch {
        Write-Status "Failed to check for updates: $_" "Red"
    }
}

function Update-Packages {
    Write-Header "Checking for Updates"
    
    try {
        $updatePackages = Get-WinGetPackage -Source "winget" | 
            Where-Object IsUpdateAvailable -eq $true |
            Select-Object Id, Name, InstalledVersion, @{Name='LastVersion'; Expression={$_.AvailableVersions[0]}}

        if (-not $updatePackages) {
            Write-Warning "No updates available"
            return
        }

        Write-Success "Found $($updatePackages.count) updates available"
        
        $excludedPackages = Get-ExcludedPackages
        $packagesToInstall = $updatePackages | Where-Object { $excludedPackages -notcontains $_.Id }
        $excludedToInstall = $updatePackages | Where-Object { $excludedPackages -contains $_.Id }

        if ($excludedToInstall) {
            Write-Header "Excluded from Updates"
            $excludedPackageIds = ($excludedToInstall.Id) -join ', '
            Write-Warning "Excluded packages: $excludedPackageIds"
        }

        if ($packagesToInstall) {
            Write-Header "Available Updates"
            # Intestazione tabella
            Write-Host ("{0,-5}{1,-60}{2,-20}{3,-20}" -f "#", "Package ID", "Current Version", "New Version") -ForegroundColor Cyan
            Write-Host ("{0,-5}{1,-60}{2,-20}{3,-20}" -f ("-"*1), ("-"*10), ("-"*14), ("-"*10)) -ForegroundColor Cyan
            
            $i = 1
            foreach ($pkg in $packagesToInstall) {
                # Tronca l'ID se troppo lungo
                $displayId = if ($pkg.Id.Length -gt 57) {
                    $pkg.Id.Substring(0, 54) + "..."
                } else {
                    $pkg.Id
                }
                Write-Host -NoNewline ("{0,-5}" -f $i) -ForegroundColor Magenta
                Write-Host -NoNewline ("{0,-60}" -f $displayId)
                Write-Host -NoNewline ("{0,-20}" -f $pkg.InstalledVersion) -ForegroundColor Red
                Write-Host ("{0,-20}" -f $pkg.LastVersion) -ForegroundColor Green
                $i++
            }

            $response = Read-Host "`nDo you want to proceed with installing $($packagesToInstall.Count) updates? (y/n)"
            if ($response -ieq "y") {
                $results = @()

                foreach ($pkg in $packagesToInstall) {
                    Write-Progress -Message "Updating $($pkg.Id)" -Percent 0
                    Start-Sleep -Milliseconds 500
                    
                    try {
                        Write-Progress -Message "Updating $($pkg.Id)" -Percent 50
                        $result = $pkg | Update-WinGetPackage -Mode Silent -ProgressAction SilentlyContinue
                        
                        Write-Progress -Message "Updating $($pkg.Id)" -Percent 100
                        Start-Sleep -Milliseconds 500
                        
                        $results += [PSCustomObject]@{
                            Id = $pkg.Id
                            LastVersion = $pkg.LastVersion
                            Status = $result.Status
                            Success = ($result.Status -ieq "OK")
                        }
                    } catch {
                        $results += [PSCustomObject]@{
                            Id = $pkg.Id
                            LastVersion = $pkg.LastVersion
                            Status = $_.Exception.Message
                            Success = $false
                        }
                    }
                }

                Write-Header "Update Results"
                foreach ($result in $results) {
                    if ($result.Success) {
                        Write-Success "Updated $($result.Id) to version $($result.LastVersion)"
                    } else {
                        Write-Error "Failed to update $($result.Id). Error: $($result.Status)"
                    }
                }
            }
        }
    } catch {
        Write-Error "Failed to update packages: $_"
    }
}

function Get-ExcludedPackages {
    if ($XclusionList.IsPresent) {
        Write-Header "Excluded Packages List"
        if (Test-Path $JSON_FILE_PATH) {
            try {
                $jsonContent = Get-Content -Raw -Path $JSON_FILE_PATH | ConvertFrom-Json
                if ($jsonContent.ExcludedPackages -and $jsonContent.ExcludedPackages.Count -gt 0) {
                    # Intestazione tabella
                    Write-Host ("{0,-5}{1,-60}" -f "#", "Package ID") -ForegroundColor Cyan
                    Write-Host ("{0,-5}{1,-60}" -f ("-"*1), ("-"*10)) -ForegroundColor Cyan
                    
                    $i = 1
                    $jsonContent.ExcludedPackages | Sort-Object -Unique | ForEach-Object {
                        # Tronca l'ID se troppo lungo
                        $displayId = if ($_.Length -gt 57) {
                            $_.Substring(0, 54) + "..."
                        } else {
                            $_
                        }
                        Write-Host -NoNewline ("{0,-5}" -f $i) -ForegroundColor Magenta
                        Write-Host ("{0,-60}" -f $displayId)
                        $i++
                    }
                    Write-Success "Found $($jsonContent.ExcludedPackages.Count) excluded packages"
                } else {
                    Write-Warning "No packages are excluded for update right now"
                }
            } catch {
                Write-Error "Failed to read exclusion list: $_"
            }
        }
        return
    }

    try {
        if (Test-Path $JSON_FILE_PATH) {
            $jsonContent = Get-Content -Raw -Path $JSON_FILE_PATH | ConvertFrom-Json
            if ($jsonContent.ExcludedPackages -is [string]) {
                # Convert single string to array if needed
                return @($jsonContent.ExcludedPackages)
            }
            return $jsonContent.ExcludedPackages -as [string[]]
        } else {
            $initialJson = @{
                ExcludedPackages = @()
            } | ConvertTo-Json
            $initialJson | Set-Content -Path $JSON_FILE_PATH
            return @()
        }
    } catch {
        Write-Error "Failed to get excluded packages: $_"
        return @()
    }
}

function Add-ExcludedPackage {
    param([string]$packageId)
    
    try {
        $excludedPackages = Get-ExcludedPackages
        if ($excludedPackages -is [string]) {
            $excludedPackages = @($excludedPackages)
        }
        
        if ($packageId -notin $excludedPackages) {
            $excludedPackages = @($excludedPackages) + $packageId
            $jsonContent = @{
                ExcludedPackages = $excludedPackages
            }
            $jsonContent | ConvertTo-Json | Set-Content -Path $JSON_FILE_PATH
            Write-Success "Package $packageId is excluded from future updates"
        } else {
            Write-Warning "Package $packageId is already excluded from future update"
        }
    } catch {
        Write-Error "Failed to add excluded package: $_"
    }
}

function Remove-ExcludedPackage {
    param([string]$packageId)
    
    try {
        $excludedPackages = Get-ExcludedPackages
        if ($excludedPackages -is [string]) {
            $excludedPackages = @($excludedPackages)
        }
        
        if ($packageId -in $excludedPackages) {
            $excludedPackages = $excludedPackages | Where-Object { $_ -ne $packageId }
            $jsonContent = @{
                ExcludedPackages = $excludedPackages
            }
            $jsonContent | ConvertTo-Json | Set-Content -Path $JSON_FILE_PATH
            Write-Success "Package $packageId has been removed from the exclusion list"
        } else {
            Write-Warning "Package $packageId is not in the exclusion list"
        }
    } catch {
        Write-Error "Failed to remove excluded package: $_"
    }
}

function Install-Packages {
    param(
        [string]$PackageId,
        [string]$Version = $null
    )
    Write-Header "Package Installation"
    
    try {
        if (-not (Get-WinGetPackage | Where-Object Id -eq $PackageId)) {
            Write-Status "Starting installation of $PackageId"
            $params = @{
                Id = $PackageId
                Mode = "Silent"
            }
            if ($Version) {
                $params.Version = $Version
            }
            
            $result = Install-WinGetPackage @params
            if ($result.Status -ieq "OK") {
                Write-Status "The package $PackageId is now installed in your system" "Green"
            } else {
                Write-Status "Installation failed for $PackageId. Error: $($result.Status)" "Red"
            }
        } else {
            Write-Status "Package $PackageId is already installed in the system" "Yellow"
        }
    } catch {
        Write-Status "Installation failed: $_" "Red"
    }
}

function Find-Packages {
    param([string]$SearchTerm)
    Write-Header "Package Search Results"
    Write-Status "Searching package required" "Yellow"
    try {
        $foundPackages = Find-WinGetPackage $SearchTerm -Source winget | 
            Select-Object Name, Id, Version, AvailableVersions
        
        if ($foundPackages) {
            # Intestazione tabella
            Write-Host ("{0,-5}{1,-60}{2,-20}{3,-20}" -f "#", "Package ID", "Version", "Latest Versions") -ForegroundColor Cyan
            Write-Host ("{0,-5}{1,-60}{2,-20}{3,-20}" -f ("-"*1), ("-"*10), ("-"*7), ("-"*13)) -ForegroundColor Cyan
            
            $i = 1
            foreach ($pkg in $foundPackages) {
                # Tronca l'ID se troppo lungo
                $displayId = if ($pkg.Id.Length -gt 57) {
                    $pkg.Id.Substring(0, 54) + "..."
                } else {
                    $pkg.Id
                }
                # Prendi solo le ultime 7 versioni
                $latestVersions = if ($pkg.AvailableVersions.Count -gt 7) {
                    ($pkg.AvailableVersions | Select-Object -First 7) -join ", "
                } else {
                    $pkg.AvailableVersions -join ", "
                }
                
                Write-Host -NoNewline ("{0,-5}" -f $i) -ForegroundColor Magenta
                Write-Host -NoNewline ("{0,-60}" -f $displayId)
                Write-Host -NoNewline ("{0,-20}" -f $pkg.Version) -ForegroundColor Red
                Write-Host ("{0,-20}" -f $latestVersions) -ForegroundColor Yellow
                $i++
            }
            Write-Status "Search completed" "Green"
        } else {
            Write-Status "No packages with name $SearchTerm was found in winget" "Red"
        }
    } catch {
        Write-Status "Search failed: $_" "Red"
    }
}

function Remove-Packages {
    param([string]$PackageId)
    Write-Header "Package Removal"
    Write-Status "Searching package to remove" "Yellow"
    try {
        $installedPackage = Get-WinGetPackage | Where-Object Id -eq $PackageId
        if ($installedPackage) {
            $response = Read-Host ":: You are uninstalling $PackageId from your system. Do you want to proceed? (y/n)"
            if ($response -ieq "y") {
                $result = Uninstall-WinGetPackage -Id $PackageId -Mode Silent
                if ($result.Status -ieq "OK") {
                    Write-Status "The package $PackageId is now uninstalled from your system" "Green"
                } else {
                    Write-Status "Uninstallation failed for $PackageId. Error: $($result.Status)" "Red"
                }
            }
        } else {
            Write-Status "No packages with name $PackageId was found in your system" "Red"
        }
    } catch {
        Write-Status "Uninstallation failed: $_" "Red"
    }
}

function Get-ListPackages {
    Write-Header "Installed Packages"
    Write-Status "Listing all packages installed" "Yellow"
    try {
        $packages = Get-WinGetPackage -Source "winget" | Select-Object Id, InstalledVersion
        if ($packages) {
            # Intestazione tabella con larghezza aumentata per PackageID
            Write-Host ("{0,-5}{1,-60}{2,-20}" -f "#", "Package ID", "Version") -ForegroundColor Cyan
            Write-Host ("{0,-5}{1,-60}{2,-20}" -f ("-"*1), ("-"*10), ("-"*7)) -ForegroundColor Cyan
            $i = 1
            foreach ($pkg in $packages) {
                # Tronca l'ID se troppo lungo
                $displayId = if ($pkg.Id.Length -gt 57) {
                    $pkg.Id.Substring(0, 54) + "..."
                } else {
                    $pkg.Id
                }
                Write-Host -NoNewline ("{0,-5}" -f $i) -ForegroundColor Magenta
                Write-Host -NoNewline ("{0,-60}" -f $displayId)
                Write-Host ("{0,-20}" -f $pkg.InstalledVersion) -ForegroundColor Red
                $i++
            }
        } else {
            Write-Warning "No packages found."
        }
        Write-Status "Listing completed" "Green"
    } catch {
        Write-Status "Failed to list packages: $_" "Red"
    }
}

function Show-Help {
    Write-Header "Help"
    @"
Usage: winpkg [options]
 
Options:
  -U, --update Update all available packages
  -I, --install <package> Install a specific package
  -V, --version <version> Specify version for installation
  -F, --find <package> Search for packages
  -L, --list List all installed packages
  -R, --remove <package> Remove a package
  -E, --exclude <package> Exclude package from updates
  -P, --process <package> Process excluded package
  -X, --exclusion-list Show excluded packages list
  -H, --help Show this help message
 
Examples:
  winpkg -U # Update all packages
  winpkg -I "Microsoft.VisualStudioCode" # Install VS Code
  winpkg -F "python" # Search for Python packages
  winpkg -L # List installed packages
"@
 | Out-Host
    Write-Host $SEPARATOR
}

# Main execution
Write-Host $TITLE -ForegroundColor Cyan
Write-Host $SEPARATOR
Get-WinPKGUpdates

# Process command line arguments
if ($Update.IsPresent) {
    Update-Packages
}
elseif ($Install) {
    Install-Packages -PackageId $Install -Version $Version
}
elseif ($Find) {
    Find-Packages -SearchTerm $Find
}
elseif ($Exclude) {
    Add-ExcludedPackage -packageId $Exclude
}
elseif ($List.IsPresent) {
    Get-ListPackages
}
elseif ($Remove) {
    Remove-Packages -PackageId $Remove
}
elseif ($Process) {
    Remove-ExcludedPackage -packageId $Process
}
elseif ($Help.IsPresent) {
    Show-Help
}
elseif ($XclusionList.IsPresent) {
    Get-ExcludedPackages
}
else {
    Show-Help
}