Public/Get-WingetRecommend.ps1
|
function Get-WingetRecommend { <# .SYNOPSIS AI-powered package recommendations using local heuristic matching. .DESCRIPTION Recommends packages based on a persona/role description or by analyzing your current installed packages ("clone this machine's personality"). Uses a curated knowledge base of package archetypes mapped to roles, combined with co-occurrence analysis of your installed packages to suggest complementary software you're likely missing. No external AI/LLM required — pure local heuristic matching that feels magic. .PARAMETER Persona A role or description of what you do. Examples: "backend developer", "data scientist", "gamer", "devops engineer", "web developer", "game developer", "security researcher", "student" .PARAMETER ClonePersonality Analyze installed packages and recommend complementary ones you're missing. Uses co-occurrence patterns and archetype matching. .PARAMETER Category Filter recommendations to a category: Development, Productivity, Gaming, Design, DevOps, Security, Media, Utilities. .PARAMETER MaxResults Maximum recommendations to return. Default: 20. .PARAMETER ExcludeInstalled Hide packages already installed. Default: true. .PARAMETER Install Interactively select and install recommended packages. .PARAMETER Explain Show why each package was recommended (matching reasoning). .EXAMPLE Get-WingetRecommend -Persona "backend developer" Recommends packages for a backend dev workflow. .EXAMPLE Get-WingetRecommend -ClonePersonality Analyzes your machine and suggests what's missing. .EXAMPLE Get-WingetRecommend -Persona "data scientist" -Install Shows recommendations then lets you pick which to install. .EXAMPLE Get-WingetRecommend -Category DevOps -Explain DevOps packages with reasoning for each recommendation. .NOTES Author: Matthew Bubb The recommendation engine uses a local archetype database with co-occurrence scoring. No data leaves your machine. #> [CmdletBinding(DefaultParameterSetName = 'Persona')] param( [Parameter(ParameterSetName = 'Persona', Mandatory, Position = 0)] [string]$Persona, [Parameter(ParameterSetName = 'Clone', Mandatory)] [switch]$ClonePersonality, [Parameter()] [ValidateSet('Development', 'Productivity', 'Gaming', 'Design', 'DevOps', 'Security', 'Media', 'Utilities')] [string]$Category, [ValidateRange(5, 100)] [int]$MaxResults = 20, [bool]$ExcludeInstalled = $true, [switch]$Install, [switch]$Explain ) # --- ARCHETYPE KNOWLEDGE BASE --- # Each archetype: keywords that match, packages with weights and reasons $archetypes = @{ 'Backend Developer' = @{ Keywords = @('backend', 'api', 'server', 'microservice', 'database', 'sql', 'rest', 'grpc', 'dotnet', 'java', 'python', 'go', 'rust', 'node') Packages = @( @{ Id = 'Git.Git'; Weight = 10; Reason = 'Version control foundation' } @{ Id = 'Docker.DockerDesktop'; Weight = 9; Reason = 'Container-based service orchestration' } @{ Id = 'Microsoft.VisualStudioCode'; Weight = 9; Reason = 'Primary code editor with extensions' } @{ Id = 'JetBrains.IntelliJIDEA.Community'; Weight = 7; Reason = 'JVM language IDE' } @{ Id = 'Python.Python.3.12'; Weight = 8; Reason = 'Scripting and service tooling' } @{ Id = 'GoLang.Go'; Weight = 7; Reason = 'High-performance service language' } @{ Id = 'Rustlang.Rustup'; Weight = 6; Reason = 'Systems programming language' } @{ Id = 'PostgreSQL.PostgreSQL'; Weight = 8; Reason = 'Relational database server' } @{ Id = 'Redis.Redis'; Weight = 7; Reason = 'In-memory cache and message broker' } @{ Id = 'Microsoft.DotNet.SDK.8'; Weight = 8; Reason = '.NET runtime and SDK' } @{ Id = 'OpenJS.NodeJS.LTS'; Weight = 8; Reason = 'JavaScript runtime for tooling' } @{ Id = 'JetBrains.DataGrip'; Weight = 6; Reason = 'Database IDE and query tool' } @{ Id = 'Postman.Postman'; Weight = 7; Reason = 'API testing and documentation' } @{ Id = 'Grafana.Agent'; Weight = 5; Reason = 'Observability and metrics' } @{ Id = 'Kubernetes.kubectl'; Weight = 6; Reason = 'Container orchestration CLI' } ) } 'Frontend Developer' = @{ Keywords = @('frontend', 'web', 'react', 'vue', 'angular', 'css', 'html', 'javascript', 'typescript', 'ui', 'ux', 'svelte', 'nextjs') Packages = @( @{ Id = 'Microsoft.VisualStudioCode'; Weight = 10; Reason = 'Editor with best JS/TS support' } @{ Id = 'OpenJS.NodeJS.LTS'; Weight = 10; Reason = 'JavaScript runtime and npm ecosystem' } @{ Id = 'Git.Git'; Weight = 9; Reason = 'Version control' } @{ Id = 'Google.Chrome'; Weight = 8; Reason = 'Primary dev browser with DevTools' } @{ Id = 'Mozilla.Firefox.DeveloperEdition'; Weight = 7; Reason = 'Secondary testing browser' } @{ Id = 'Figma.Figma'; Weight = 7; Reason = 'Design-to-code collaboration' } @{ Id = 'Vercel.nextjs'; Weight = 6; Reason = 'React framework tooling' } @{ Id = 'Yarn.Yarn'; Weight = 6; Reason = 'Alternative package manager' } @{ Id = 'Microsoft.Edge.Dev'; Weight = 5; Reason = 'Chromium testing channel' } @{ Id = 'Nginx.Nginx'; Weight = 5; Reason = 'Local reverse proxy for dev' } ) } 'Data Scientist' = @{ Keywords = @('data', 'science', 'ml', 'machine learning', 'ai', 'analytics', 'jupyter', 'pandas', 'numpy', 'tensorflow', 'pytorch', 'statistics', 'visualization') Packages = @( @{ Id = 'Python.Python.3.12'; Weight = 10; Reason = 'Primary data science runtime' } @{ Id = 'Anaconda.Anaconda3'; Weight = 9; Reason = 'Scientific computing distribution' } @{ Id = 'Microsoft.VisualStudioCode'; Weight = 8; Reason = 'Editor with Jupyter integration' } @{ Id = 'Git.Git'; Weight = 8; Reason = 'Experiment versioning' } @{ Id = 'Docker.DockerDesktop'; Weight = 7; Reason = 'Reproducible environments' } @{ Id = 'PostgreSQL.PostgreSQL'; Weight = 7; Reason = 'Data warehouse' } @{ Id = 'RProject.R'; Weight = 8; Reason = 'Statistical computing language' } @{ Id = 'RStudio.RStudio.OpenSource'; Weight = 7; Reason = 'R IDE and notebooks' } @{ Id = 'JetBrains.PyCharm.Community'; Weight = 7; Reason = 'Python IDE for ML projects' } @{ Id = 'Microsoft.PowerBI'; Weight = 6; Reason = 'Business intelligence dashboards' } @{ Id = 'Tableau.TableauDesktop'; Weight = 5; Reason = 'Advanced data visualization' } @{ Id = 'Julia.Julia'; Weight = 6; Reason = 'High-performance numerical computing' } ) } 'DevOps Engineer' = @{ Keywords = @('devops', 'infrastructure', 'ci/cd', 'pipeline', 'terraform', 'ansible', 'kubernetes', 'cloud', 'aws', 'azure', 'monitoring', 'sre', 'platform') Packages = @( @{ Id = 'Git.Git'; Weight = 10; Reason = 'Infrastructure-as-code versioning' } @{ Id = 'Hashicorp.Terraform'; Weight = 9; Reason = 'Infrastructure provisioning' } @{ Id = 'Kubernetes.kubectl'; Weight = 9; Reason = 'Cluster management' } @{ Id = 'Docker.DockerDesktop'; Weight = 9; Reason = 'Container development' } @{ Id = 'Microsoft.AzureCLI'; Weight = 8; Reason = 'Azure cloud management' } @{ Id = 'Amazon.AWSCLI'; Weight = 8; Reason = 'AWS cloud management' } @{ Id = 'Helm.Helm'; Weight = 7; Reason = 'Kubernetes package manager' } @{ Id = 'Ansible.Ansible'; Weight = 7; Reason = 'Configuration management' } @{ Id = 'Grafana.Grafana'; Weight = 7; Reason = 'Monitoring dashboards' } @{ Id = 'Hashicorp.Vault'; Weight = 6; Reason = 'Secrets management' } @{ Id = 'Microsoft.VisualStudioCode'; Weight = 7; Reason = 'IaC editing' } @{ Id = 'PuTTY.PuTTY'; Weight = 5; Reason = 'SSH access to servers' } @{ Id = 'WiresharkFoundation.Wireshark'; Weight = 6; Reason = 'Network troubleshooting' } @{ Id = 'Python.Python.3.12'; Weight = 7; Reason = 'Automation scripting' } ) } 'Gamer' = @{ Keywords = @('gaming', 'gamer', 'game', 'steam', 'twitch', 'discord', 'rgb', 'fps', 'esports', 'streaming', 'obs') Packages = @( @{ Id = 'Valve.Steam'; Weight = 10; Reason = 'Primary game library' } @{ Id = 'Discord.Discord'; Weight = 9; Reason = 'Voice chat and communities' } @{ Id = 'OBSProject.OBSStudio'; Weight = 8; Reason = 'Game streaming and recording' } @{ Id = 'EpicGames.EpicGamesLauncher'; Weight = 7; Reason = 'Epic game library' } @{ Id = 'RiotGames.RiotClient'; Weight = 7; Reason = 'League/Valorant client' } @{ Id = 'NVIDIA.GeForceExperience'; Weight = 8; Reason = 'GPU optimization and recording' } @{ Id = 'Elgato.StreamDeck'; Weight = 6; Reason = 'Stream control deck software' } @{ Id = 'Mozilla.Firefox'; Weight = 5; Reason = 'Gaming wikis and guides' } @{ Id = 'Spotify.Spotify'; Weight = 6; Reason = 'Gaming music' } @{ Id = 'GOG.Galaxy'; Weight = 6; Reason = 'DRM-free game library' } @{ Id = 'Ubisoft.Connect'; Weight = 5; Reason = 'Ubisoft game library' } ) } 'Game Developer' = @{ Keywords = @('game dev', 'game development', 'unity', 'unreal', 'godot', 'gamedev', 'indie', 'shader', '3d', 'blender', 'pixel art') Packages = @( @{ Id = 'Unity.Unity'; Weight = 10; Reason = 'Game engine (C#)' } @{ Id = 'EpicGames.UnrealEngine'; Weight = 9; Reason = 'AAA game engine (C++/Blueprints)' } @{ Id = 'GodotEngine.GodotEngine'; Weight = 9; Reason = 'Open-source game engine' } @{ Id = 'BlenderFoundation.Blender'; Weight = 9; Reason = '3D modeling and animation' } @{ Id = 'Microsoft.VisualStudioCode'; Weight = 8; Reason = 'Scripting and shader editing' } @{ Id = 'Git.Git'; Weight = 8; Reason = 'Asset and code versioning' } @{ Id = 'GIMP.GIMP'; Weight = 7; Reason = '2D art and texture editing' } @{ Id = 'Inkscape.Inkscape'; Weight = 6; Reason = 'Vector art for UI/sprites' } @{ Id = 'Audacity.Audacity'; Weight = 6; Reason = 'Audio editing for SFX' } @{ Id = 'Microsoft.DotNet.SDK.8'; Weight = 7; Reason = 'C# development for Unity' } @{ Id = 'Microsoft.VisualStudio.Community'; Weight = 7; Reason = 'Full C++ IDE for Unreal' } ) } 'Security Researcher' = @{ Keywords = @('security', 'pentest', 'hacking', 'ctf', 'forensics', 'malware', 'reverse engineering', 'vulnerability', 'red team', 'blue team', 'soc') Packages = @( @{ Id = 'WiresharkFoundation.Wireshark'; Weight = 10; Reason = 'Network packet analysis' } @{ Id = 'Python.Python.3.12'; Weight = 9; Reason = 'Exploit scripting and automation' } @{ Id = 'Git.Git'; Weight = 8; Reason = 'Tool and research versioning' } @{ Id = 'Nmap.Nmap'; Weight = 9; Reason = 'Network scanning and enumeration' } @{ Id = 'GnuPG.GnuPG'; Weight = 7; Reason = 'Encryption and signing' } @{ Id = 'KeePassXCTeam.KeePassXC'; Weight = 7; Reason = 'Credential management' } @{ Id = 'Microsoft.VisualStudioCode'; Weight = 7; Reason = 'Code analysis and scripting' } @{ Id = 'Docker.DockerDesktop'; Weight = 7; Reason = 'Isolated malware analysis labs' } @{ Id = 'Oracle.VirtualBox'; Weight = 8; Reason = 'Vulnerable VM labs' } @{ Id = 'Hex-Rays.IDA.Free'; Weight = 8; Reason = 'Binary reverse engineering' } @{ Id = 'Ghidra.Ghidra'; Weight = 8; Reason = 'NSA reverse engineering suite' } @{ Id = 'Tor.TorBrowser'; Weight = 6; Reason = 'Anonymous research browsing' } ) } 'Designer' = @{ Keywords = @('design', 'graphic', 'ui', 'ux', 'figma', 'photoshop', 'illustrator', 'creative', 'photo', 'video', 'editing', 'adobe') Packages = @( @{ Id = 'Figma.Figma'; Weight = 10; Reason = 'UI/UX design and prototyping' } @{ Id = 'GIMP.GIMP'; Weight = 8; Reason = 'Raster image editing' } @{ Id = 'Inkscape.Inkscape'; Weight = 8; Reason = 'Vector graphics editor' } @{ Id = 'BlenderFoundation.Blender'; Weight = 7; Reason = '3D design and rendering' } @{ Id = 'DaVinciResolve.DaVinciResolve'; Weight = 7; Reason = 'Video editing and color grading' } @{ Id = 'Audacity.Audacity'; Weight = 6; Reason = 'Audio editing' } @{ Id = 'OBSProject.OBSStudio'; Weight = 6; Reason = 'Screen recording for portfolios' } @{ Id = 'Mozilla.Firefox'; Weight = 5; Reason = 'Design inspiration browsing' } @{ Id = 'Google.Chrome'; Weight = 5; Reason = 'Web design testing' } @{ Id = 'Git.Git'; Weight = 6; Reason = 'Design asset versioning' } @{ Id = 'VideoLAN.VLC'; Weight = 5; Reason = 'Media format preview' } ) } 'Productivity' = @{ Keywords = @('productivity', 'office', 'notes', 'organization', 'writing', 'email', 'calendar', 'meeting', 'remote', 'work') Packages = @( @{ Id = 'Microsoft.Office'; Weight = 9; Reason = 'Document and spreadsheet suite' } @{ Id = 'Microsoft.Teams'; Weight = 8; Reason = 'Meetings and collaboration' } @{ Id = 'Slack.Slack'; Weight = 7; Reason = 'Team communication' } @{ Id = 'Notion.Notion'; Weight = 8; Reason = 'Notes and project management' } @{ Id = 'Obsidian.Obsidian'; Weight = 8; Reason = 'Knowledge management (local-first)' } @{ Id = 'Todoist.Todoist'; Weight = 7; Reason = 'Task management' } @{ Id = 'Zoom.Zoom'; Weight = 7; Reason = 'Video conferencing' } @{ Id = 'Mozilla.Firefox'; Weight = 6; Reason = 'Research browser' } @{ Id = '7zip.7zip'; Weight = 6; Reason = 'File compression utility' } @{ Id = 'ShareX.ShareX'; Weight = 7; Reason = 'Screenshots and screen recording' } @{ Id = 'AutoHotkey.AutoHotkey'; Weight = 6; Reason = 'Workflow automation hotkeys' } ) } 'Student' = @{ Keywords = @('student', 'school', 'university', 'college', 'study', 'learning', 'course', 'homework', 'research', 'academic') Packages = @( @{ Id = 'Microsoft.VisualStudioCode'; Weight = 9; Reason = 'Free code editor for CS courses' } @{ Id = 'Git.Git'; Weight = 9; Reason = 'Assignment version control' } @{ Id = 'Python.Python.3.12'; Weight = 8; Reason = 'Intro programming language' } @{ Id = 'Obsidian.Obsidian'; Weight = 8; Reason = 'Study notes and knowledge base' } @{ Id = 'Zotero.Zotero'; Weight = 8; Reason = 'Research paper management' } @{ Id = 'Mozilla.Firefox'; Weight = 7; Reason = 'Research browser with containers' } @{ Id = 'VideoLAN.VLC'; Weight = 6; Reason = 'Lecture video playback' } @{ Id = 'Audacity.Audacity'; Weight = 5; Reason = 'Audio note recording' } @{ Id = 'GIMP.GIMP'; Weight = 5; Reason = 'Free image editing for projects' } @{ Id = '7zip.7zip'; Weight = 6; Reason = 'Extract course materials' } @{ Id = 'Discord.Discord'; Weight = 6; Reason = 'Study group communication' } ) } } # --- CO-OCCURRENCE MATRIX (commonly paired packages) --- $coOccurrence = @{ 'Git.Git' = @('Microsoft.VisualStudioCode', 'Docker.DockerDesktop', 'OpenJS.NodeJS.LTS', 'Python.Python.3.12') 'Docker.DockerDesktop' = @('Kubernetes.kubectl', 'Hashicorp.Terraform', 'Microsoft.VisualStudioCode', 'Git.Git') 'Microsoft.VisualStudioCode' = @('Git.Git', 'Python.Python.3.12', 'OpenJS.NodeJS.LTS', 'Docker.DockerDesktop') 'Python.Python.3.12' = @('Microsoft.VisualStudioCode', 'Anaconda.Anaconda3', 'Git.Git', 'JetBrains.PyCharm.Community') 'OpenJS.NodeJS.LTS' = @('Microsoft.VisualStudioCode', 'Git.Git', 'Yarn.Yarn', 'Docker.DockerDesktop') 'Valve.Steam' = @('Discord.Discord', 'OBSProject.OBSStudio', 'NVIDIA.GeForceExperience') 'WiresharkFoundation.Wireshark' = @('Nmap.Nmap', 'Python.Python.3.12', 'Oracle.VirtualBox') 'BlenderFoundation.Blender' = @('GIMP.GIMP', 'Inkscape.Inkscape', 'DaVinciResolve.DaVinciResolve') 'PostgreSQL.PostgreSQL' = @('Redis.Redis', 'JetBrains.DataGrip', 'Docker.DockerDesktop') } # --- GET INSTALLED PACKAGES --- $installedIds = @() if ($ExcludeInstalled -or $ClonePersonality) { try { $installed = Microsoft.WinGet.Client\Get-WinGetPackage -ErrorAction SilentlyContinue $installedIds = @($installed | ForEach-Object { $_.Id }) } catch { Write-Verbose "Could not enumerate installed packages: $_" } } # --- SCORING ENGINE --- $scores = @{} # PackageId -> @{ Score; Reasons; Source } function Add-Score { param([string]$Id, [double]$Weight, [string]$Reason, [string]$Source) if (-not $scores.ContainsKey($Id)) { $scores[$Id] = @{ Score = 0; Reasons = @(); Sources = @() } } $scores[$Id].Score += $Weight $scores[$Id].Reasons += $Reason $scores[$Id].Sources += $Source } if ($ClonePersonality) { # Analyze installed packages for archetype matching Write-Host "`n Analyzing $($installedIds.Count) installed packages..." -ForegroundColor Cyan # Score each archetype by how many of its packages you already have $archetypeScores = @{} foreach ($arch in $archetypes.GetEnumerator()) { $matchCount = ($arch.Value.Packages | Where-Object { $_.Id -in $installedIds }).Count $totalPkgs = $arch.Value.Packages.Count $archetypeScores[$arch.Key] = $matchCount / [Math]::Max($totalPkgs, 1) } # Top matching archetypes $topArchetypes = $archetypeScores.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 3 Write-Host " Detected personality: " -NoNewline -ForegroundColor DarkGray Write-Host ($topArchetypes | ForEach-Object { "$($_.Key) ($([Math]::Round($_.Value * 100))%)" }) -ForegroundColor Yellow -Separator ", " # Recommend missing packages from top archetypes foreach ($arch in $topArchetypes) { $archetype = $archetypes[$arch.Key] foreach ($pkg in $archetype.Packages) { if ($pkg.Id -notin $installedIds) { $adjustedWeight = $pkg.Weight * $arch.Value # Scale by archetype match Add-Score -Id $pkg.Id -Weight $adjustedWeight -Reason "$($pkg.Reason) [$($arch.Key)]" -Source 'archetype' } } } # Co-occurrence recommendations foreach ($instId in $installedIds) { if ($coOccurrence.ContainsKey($instId)) { foreach ($paired in $coOccurrence[$instId]) { if ($paired -notin $installedIds) { Add-Score -Id $paired -Weight 4 -Reason "Commonly paired with $instId" -Source 'co-occurrence' } } } } } else { # Persona-based matching $personaLower = $Persona.ToLower() $matchedArchetypes = @() foreach ($arch in $archetypes.GetEnumerator()) { $keywordHits = ($arch.Value.Keywords | Where-Object { $personaLower -match [regex]::Escape($_) }).Count if ($keywordHits -gt 0) { $matchedArchetypes += @{ Name = $arch.Key; Relevance = $keywordHits / $arch.Value.Keywords.Count; Data = $arch.Value } } } if ($matchedArchetypes.Count -eq 0) { # Fuzzy fallback: check for partial matches foreach ($arch in $archetypes.GetEnumerator()) { $nameWords = $arch.Key.ToLower().Split(' ') $personaWords = $personaLower.Split(' ') $overlap = ($nameWords | Where-Object { $_ -in $personaWords }).Count if ($overlap -gt 0) { $matchedArchetypes += @{ Name = $arch.Key; Relevance = $overlap / $nameWords.Count; Data = $arch.Value } } } } if ($matchedArchetypes.Count -eq 0) { Write-Host "`n No exact archetype match for '$Persona'." -ForegroundColor Yellow Write-Host " Available personas: $($archetypes.Keys -join ', ')" -ForegroundColor DarkGray Write-Host " Try a more specific description or use -ClonePersonality.`n" -ForegroundColor DarkGray return } # Sort by relevance and show matches $matchedArchetypes = $matchedArchetypes | Sort-Object { $_.Relevance } -Descending Write-Host "`n Matched archetypes: " -NoNewline -ForegroundColor DarkGray Write-Host ($matchedArchetypes | ForEach-Object { "$($_.Name) ($([Math]::Round($_.Relevance * 100))%)" }) -ForegroundColor Cyan -Separator ", " # Score packages from matched archetypes foreach ($match in $matchedArchetypes) { foreach ($pkg in $match.Data.Packages) { $adjustedWeight = $pkg.Weight * $match.Relevance Add-Score -Id $pkg.Id -Weight $adjustedWeight -Reason "$($pkg.Reason) [$($match.Name)]" -Source 'persona' } } # Boost with co-occurrence from installed packages foreach ($instId in $installedIds) { if ($coOccurrence.ContainsKey($instId)) { foreach ($paired in $coOccurrence[$instId]) { if ($scores.ContainsKey($paired)) { $scores[$paired].Score += 2 # Boost already-recommended } } } } } # --- FILTER --- $recommendations = $scores.GetEnumerator() | Where-Object { -not $ExcludeInstalled -or $_.Key -notin $installedIds } | Sort-Object { $_.Value.Score } -Descending | Select-Object -First $MaxResults # Category filter if ($Category) { # Simple category mapping by package ID patterns $categoryMap = @{ 'Development' = @('Git', 'Code', 'Python', 'Node', 'DotNet', 'JetBrains', 'Rust', 'Go', 'Java') 'DevOps' = @('Docker', 'Kubernetes', 'Terraform', 'Ansible', 'Helm', 'Grafana', 'Vault', 'Azure', 'AWS') 'Gaming' = @('Steam', 'Discord', 'Epic', 'Riot', 'OBS', 'GeForce', 'GOG', 'Ubisoft') 'Design' = @('Figma', 'GIMP', 'Inkscape', 'Blender', 'DaVinci', 'Audacity') 'Security' = @('Wireshark', 'Nmap', 'Ghidra', 'IDA', 'KeePass', 'GnuPG', 'Tor', 'VirtualBox') 'Productivity' = @('Office', 'Teams', 'Slack', 'Notion', 'Obsidian', 'Zoom', 'Todoist', 'ShareX') 'Media' = @('VLC', 'OBS', 'Audacity', 'DaVinci', 'Spotify', 'GIMP') 'Utilities' = @('7zip', 'AutoHotkey', 'PuTTY', 'ShareX') } $patterns = $categoryMap[$Category] ?? @() if ($patterns.Count -gt 0) { $recommendations = $recommendations | Where-Object { $id = $_.Key ($patterns | Where-Object { $id -match [regex]::Escape($_) }).Count -gt 0 } } } # --- OUTPUT --- if ($recommendations.Count -eq 0) { Write-Host "`n No recommendations found. Your machine is complete!`n" -ForegroundColor Green return } Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════╗" -ForegroundColor Magenta Write-Host " ║ WingetBatch Recommendations ║" -ForegroundColor Magenta Write-Host " ╚══════════════════════════════════════════════════════════╝" -ForegroundColor Magenta Write-Host "" $i = 0 foreach ($rec in $recommendations) { $i++ $scoreBar = '#' * [Math]::Min([Math]::Round($rec.Value.Score), 20) $scoreBar = $scoreBar.PadRight(20, '.') Write-Host " $($i.ToString().PadLeft(2)). " -NoNewline -ForegroundColor White Write-Host $rec.Key -NoNewline -ForegroundColor Cyan Write-Host " [$scoreBar] " -NoNewline -ForegroundColor DarkGray Write-Host "$([Math]::Round($rec.Value.Score, 1))pts" -ForegroundColor Yellow if ($Explain) { foreach ($reason in ($rec.Value.Reasons | Select-Object -Unique)) { Write-Host " → $reason" -ForegroundColor DarkGray } } } Write-Host "" Write-Host " $($recommendations.Count) recommendations | Sources: $(($recommendations | ForEach-Object { $_.Value.Sources } | Select-Object -Unique) -join ', ')" -ForegroundColor DarkGray Write-Host "" # --- INTERACTIVE INSTALL --- if ($Install) { $choices = $recommendations | ForEach-Object { $_.Key } if (Get-Command Read-SpectreMultiSelection -ErrorAction SilentlyContinue) { $selected = Read-SpectreMultiSelection -Choices $choices -Title "Select packages to install" } else { Write-Host " Enter numbers to install (comma-separated, or 'all'): " -NoNewline -ForegroundColor Yellow $input = Read-Host if ($input -eq 'all') { $selected = $choices } else { $indices = $input.Split(',') | ForEach-Object { [int]$_.Trim() - 1 } $selected = $indices | ForEach-Object { $choices[$_] } } } if ($selected -and $selected.Count -gt 0) { Write-Host "`n Installing $($selected.Count) packages...`n" -ForegroundColor Green $j = 0 foreach ($pkgId in $selected) { $j++ Write-Host " [$j/$($selected.Count)] Installing $pkgId..." -NoNewline -ForegroundColor Cyan try { Microsoft.WinGet.Client\Install-WinGetPackage -Id $pkgId -Mode Silent | Out-Null Write-Host " ✓" -ForegroundColor Green } catch { Write-Host " ✗ ($($_.Exception.Message))" -ForegroundColor Red } } Write-Host "`n Done! $($selected.Count) packages processed.`n" -ForegroundColor Green } } # Return structured data for pipeline use $recommendations | ForEach-Object { [PSCustomObject]@{ PackageId = $_.Key Score = [Math]::Round($_.Value.Score, 2) Reasons = ($_.Value.Reasons | Select-Object -Unique) Sources = ($_.Value.Sources | Select-Object -Unique) } } } |