├── images └── demo.png ├── .gitignore ├── NuGet.config ├── .config └── tsaoptions.json ├── CODE_OF_CONDUCT.md ├── src ├── ValidateOS.psm1 ├── Microsoft.WinGet.CommandNotFound.psd1 ├── PooledPowerShellObjectPolicy.cs ├── Microsoft.WinGet.CommandNotFound.csproj └── WinGetCommandNotFoundFeedbackPredictor.cs ├── LICENSE ├── SUPPORT.md ├── SECURITY.md ├── README.md ├── tools └── helper.psm1 ├── Microsoft.WinGet.CommandNotFound.build.ps1 └── .pipelines └── releaseBuild.yml /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denelon/winget-command-not-found/HEAD/images/demo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build outputs 2 | bin 3 | obj 4 | src/bin 5 | src/obj 6 | 7 | # Visual Studio cache/options directory 8 | .vs/ -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.config/tsaoptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "instanceUrl": "https://microsoft.visualstudio.com", 3 | "projectName": "OS", 4 | "areaPath": "OS\\Windows Client and Services\\ADEPT\\E4D-Engineered for Developers\\SHINE", 5 | "notificationAliases": [ 6 | "cazamor@microsoft.com", 7 | "duhowett@microsoft.com" 8 | ], 9 | "template": "VSTS_Microsoft_OSGS" 10 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /src/ValidateOS.psm1: -------------------------------------------------------------------------------- 1 | function Test-WinGetExists() { 2 | $package = Get-Command winget -ErrorAction SilentlyContinue; 3 | return $package -ne $null; 4 | } 5 | 6 | if (!$IsWindows -or !(Test-WinGetExists)) { 7 | $exception = [System.PlatformNotSupportedException]::new( 8 | "This module only works on Windows and depends on the application 'winget.exe' to be available.") 9 | $err = [System.Management.Automation.ErrorRecord]::new($exception, "PlatformNotSupported", "InvalidOperation", $null) 10 | throw $err 11 | } 12 | -------------------------------------------------------------------------------- /src/Microsoft.WinGet.CommandNotFound.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '1.0.4' 3 | GUID = '7936322d-30fe-410f-b681-114fe84a65d4' 4 | Author = 'Microsoft Corporation' 5 | CompanyName = "Microsoft Corporation" 6 | Copyright = "Copyright (c) Microsoft Corporation." 7 | Description = 'Enable suggestions on how to install missing commands via winget' 8 | PowerShellVersion = '7.4' 9 | 10 | NestedModules = @('ValidateOS.psm1', 'Microsoft.WinGet.CommandNotFound.dll') 11 | FunctionsToExport = @() 12 | CmdletsToExport = @() 13 | VariablesToExport = '*' 14 | AliasesToExport = @() 15 | 16 | RequiredModules = @(@{ModuleName = 'Microsoft.WinGet.Client'; ModuleVersion = "1.8.1133"; }) 17 | 18 | PrivateData = @{ 19 | PSData = @{ 20 | Tags = @('Windows') 21 | ProjectUri = 'https://github.com/Microsoft/winget-command-not-found' 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /src/PooledPowerShellObjectPolicy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // The Microsoft Corporation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Management.Automation; 6 | using System.Management.Automation.Runspaces; 7 | using Microsoft.Extensions.ObjectPool; 8 | 9 | namespace Microsoft.WinGet.CommandNotFound 10 | { 11 | public sealed class PooledPowerShellObjectPolicy : IPooledObjectPolicy 12 | { 13 | private static readonly string[] WingetClientModuleName = new[] { "Microsoft.WinGet.Client" }; 14 | 15 | private static readonly InitialSessionState _initialSessionState; 16 | 17 | static PooledPowerShellObjectPolicy() 18 | { 19 | _initialSessionState = InitialSessionState.CreateDefault2(); 20 | _initialSessionState.ImportPSModule(WingetClientModuleName); 21 | } 22 | 23 | public System.Management.Automation.PowerShell Create() 24 | { 25 | return System.Management.Automation.PowerShell.Create(_initialSessionState); 26 | } 27 | 28 | public bool Return(System.Management.Automation.PowerShell obj) 29 | { 30 | if (obj != null) 31 | { 32 | obj.Commands.Clear(); 33 | obj.Streams.ClearStreams(); 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Microsoft.WinGet.CommandNotFound.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | true 7 | 1.0.4 8 | 9 | 10 | false 11 | 12 | 13 | ..\bin\Microsoft.WinGet.CommandNotFound 14 | 15 | Microsoft.WinGet.CommandNotFound 16 | Microsoft.WinGet.CommandNotFound 17 | 18 | 19 | 20 | 21 | false 22 | None 23 | 24 | 25 | 26 | 28 | 29 | contentFiles 30 | all 31 | runtime; compile; build; native; analyzers; buildtransitive 32 | 33 | 34 | contentFiles 35 | all 36 | runtime; compile; build; native; analyzers; buildtransitive 37 | 38 | 39 | PreserveNewest 40 | PreserveNewest 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft.WinGet.CommandNotFound 2 | 3 | The `Microsoft.WinGet.CommandNotFound` module is a feedback provider plugin for PowerShell. This feedback provider leverages the Windows Package Manager to provide suggestions for packages to install when a native command cannot be found. 4 | 5 | The [command-line predictor][command-line-predictor] feature in PowerShell enables this module to display these WinGet packages as predictive suggestions. 6 | 7 | ![Demo](./images/demo.png) 8 | 9 | ## Requirements 10 | 11 | The `Microsoft.WinGet.CommandNotFound` PowerShell Module is built on the `IFeedbackProvider` interface, which is available with PowerShell `7.4.0-preview.2` or above. To display prediction suggestions, you need [PSReadLine 2.2.6][psreadline-226] or above. 12 | 13 | - PowerShell `7.4.0-preview.2` or above 14 | - PSReadLine `2.2.6` or above 15 | 16 | The following experimental features must be enabled: 17 | 18 | - `PSFeedbackProvider` 19 | - `PSCommandNotFoundSuggestion` 20 | 21 | They can be enabled by running the following commands: 22 | ```PowerShell 23 | Enable-ExperimentalFeature PSFeedbackProvider 24 | Enable-ExperimentalFeature PSCommandNotFoundSuggestion 25 | ``` 26 | 27 | ## Documentation 28 | 29 | PowerShell feedback providers and predictors are written in C# and registered with the PowerShell [Subsystem Plugin Model][subsystem-plugin-model]. 30 | To learn more, see "How to create a feedback provider" and ["How to create a command-line predictor"][how-to-create-predictor]. 31 | 32 | ## Contributing 33 | 34 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 35 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 36 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 37 | 38 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 39 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 40 | provided by the bot. You will only need to do this once across all repos using our CLA. 41 | 42 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 43 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 44 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 45 | 46 | ## Trademarks 47 | 48 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 49 | trademarks or logos is subject to and must follow 50 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 51 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 52 | Any use of third-party trademarks or logos are subject to those third-party's policies. 53 | 54 | [command-line-predictor]: https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/using-predictors 55 | [psreadline-226]: https://www.powershellgallery.com/packages/PSReadLine/2.2.6 56 | [subsystem-plugin-model]: https://docs.microsoft.com/powershell/scripting/learn/experimental-features#pssubsystempluginmodel 57 | [how-to-create-predictor]: https://docs.microsoft.com/powershell/scripting/dev-cross-plat/create-cmdline-predictor -------------------------------------------------------------------------------- /tools/helper.psm1: -------------------------------------------------------------------------------- 1 | 2 | $MinimalSDKVersion = '8.0.204' 3 | $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" 4 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 5 | $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } 6 | 7 | <# 8 | .SYNOPSIS 9 | Get the path of the currently running powershell executable. 10 | #> 11 | function Get-PSExePath 12 | { 13 | if (-not $Script:PSExePath) { 14 | $Script:PSExePath = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName 15 | } 16 | return $Script:PSExePath 17 | } 18 | 19 | <# 20 | .SYNOPSIS 21 | Find the dotnet SDK that meets the minimal version requirement. 22 | #> 23 | function Find-Dotnet 24 | { 25 | $dotnetFile = if ($IsWindowsEnv) { "dotnet.exe" } else { "dotnet" } 26 | $dotnetExePath = Join-Path -Path $LocalDotnetDirPath -ChildPath $dotnetFile 27 | 28 | # If dotnet is already in the PATH, check to see if that version of dotnet can find the required SDK. 29 | # This is "typically" the globally installed dotnet. 30 | $foundDotnetWithRightVersion = $false 31 | $dotnetInPath = Get-Command 'dotnet' -ErrorAction Ignore 32 | if ($dotnetInPath) { 33 | $foundDotnetWithRightVersion = Test-DotnetSDK $dotnetInPath.Source 34 | } 35 | 36 | if (-not $foundDotnetWithRightVersion) { 37 | if (Test-DotnetSDK $dotnetExePath) { 38 | Write-Warning "Can't find the dotnet SDK version $MinimalSDKVersion or higher, prepending '$LocalDotnetDirPath' to PATH." 39 | $env:PATH = $LocalDotnetDirPath + [IO.Path]::PathSeparator + $env:PATH 40 | } 41 | else { 42 | throw "Cannot find the dotnet SDK with the version $MinimalSDKVersion or higher. Please specify '-Bootstrap' to install build dependencies." 43 | } 44 | } 45 | } 46 | 47 | <# 48 | .SYNOPSIS 49 | Check if the dotnet SDK meets the minimal version requirement. 50 | #> 51 | function Test-DotnetSDK 52 | { 53 | param($dotnetExePath) 54 | 55 | if (Test-Path $dotnetExePath) { 56 | $installedVersion = & $dotnetExePath --version 57 | return $installedVersion -ge $MinimalSDKVersion 58 | } 59 | return $false 60 | } 61 | 62 | <# 63 | .SYNOPSIS 64 | Install the dotnet SDK if we cannot find an existing one. 65 | #> 66 | function Install-Dotnet 67 | { 68 | [CmdletBinding()] 69 | param( 70 | [string]$Channel = 'release', 71 | [string]$Version = $MinimalSDKVersion 72 | ) 73 | 74 | try { 75 | Find-Dotnet 76 | return # Simply return if we find dotnet SDk with the correct version 77 | } catch { } 78 | 79 | $logMsg = if (Get-Command 'dotnet' -ErrorAction Ignore) { 80 | "dotnet SDK out of date. Require '$MinimalSDKVersion' but found '$dotnetSDKVersion'. Updating dotnet." 81 | } else { 82 | "dotent SDK is not present. Installing dotnet SDK." 83 | } 84 | Write-Log $logMsg -Warning 85 | 86 | $obtainUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain" 87 | 88 | try { 89 | Remove-Item $LocalDotnetDirPath -Recurse -Force -ErrorAction Ignore 90 | $installScript = if ($IsWindowsEnv) { "dotnet-install.ps1" } else { "dotnet-install.sh" } 91 | Invoke-WebRequest -Uri $obtainUrl/$installScript -OutFile $installScript 92 | 93 | if ($IsWindowsEnv) { 94 | & .\$installScript -Channel $Channel -Version $Version 95 | } else { 96 | bash ./$installScript -c $Channel -v $Version 97 | } 98 | } 99 | finally { 100 | Remove-Item $installScript -Force -ErrorAction Ignore 101 | } 102 | } 103 | 104 | <# 105 | .SYNOPSIS 106 | Write log message for the build. 107 | #> 108 | function Write-Log 109 | { 110 | param( 111 | [string] $Message, 112 | [switch] $Warning, 113 | [switch] $Indent 114 | ) 115 | 116 | $foregroundColor = if ($Warning) { "Yellow" } else { "Green" } 117 | $indentPrefix = if ($Indent) { " " } else { "" } 118 | Write-Host -ForegroundColor $foregroundColor "${indentPrefix}${Message}" 119 | } -------------------------------------------------------------------------------- /Microsoft.WinGet.CommandNotFound.build.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # To build, make sure you've installed InvokeBuild 3 | # Install-Module -Repository PowerShellGallery -Name InvokeBuild -RequiredVersion 3.1.0 4 | # 5 | # Then: 6 | # Invoke-Build 7 | # 8 | # Or: 9 | # Invoke-Build -Task ZipRelease 10 | # 11 | # Or: 12 | # Invoke-Build -Configuration Debug 13 | # 14 | # etc. 15 | # 16 | 17 | [CmdletBinding()] 18 | param( 19 | [ValidateSet("Debug", "Release")] 20 | [string]$Configuration = (property Configuration Release), 21 | 22 | [ValidateSet("net8.0")] 23 | [string]$Framework 24 | ) 25 | 26 | Import-Module "$PSScriptRoot/tools/helper.psm1" 27 | 28 | # Final bits to release go here 29 | $targetDir = "bin/$Configuration/Microsoft.WinGet.CommandNotFound" 30 | 31 | if (-not $Framework) 32 | { 33 | $Framework = "net8.0" 34 | } 35 | 36 | Write-Verbose "Building for '$Framework'" -Verbose 37 | 38 | function ConvertTo-CRLF([string] $text) { 39 | $text.Replace("`r`n","`n").Replace("`n","`r`n") 40 | } 41 | 42 | $binaryModuleParams = @{ 43 | Inputs = { Get-ChildItem src/*.cs, src/Microsoft.WinGet.CommandNotFound.csproj } 44 | Outputs = "src/bin/$Configuration/$Framework/Microsoft.WinGet.CommandNotFound.dll" 45 | } 46 | 47 | <# 48 | Synopsis: Build main binary module 49 | #> 50 | task BuildMainModule @binaryModuleParams { 51 | exec { dotnet publish -f $Framework -c $Configuration src/Microsoft.WinGet.CommandNotFound.csproj } 52 | } 53 | 54 | <# 55 | Synopsis: Copy all of the files that belong in the module to one place in the layout for installation 56 | #> 57 | task LayoutModule BuildMainModule, { 58 | if (-not (Test-Path $targetDir -PathType Container)) { 59 | New-Item $targetDir -ItemType Directory -Force > $null 60 | } 61 | 62 | $extraFiles = @('LICENSE') 63 | 64 | foreach ($file in $extraFiles) { 65 | # ensure files have \r\n line endings as the signing tool only uses those endings to avoid mixed endings 66 | $content = Get-Content -Path $file -Raw 67 | Set-Content -Path (Join-Path $targetDir (Split-Path $file -Leaf)) -Value (ConvertTo-CRLF $content) -Force 68 | } 69 | 70 | $binPath = "src/bin/$Configuration/$Framework" 71 | Copy-Item $binPath/Microsoft.WinGet.CommandNotFound.dll $targetDir 72 | Copy-Item $binPath/ValidateOS.psm1 $targetDir 73 | 74 | if ($Configuration -eq 'Debug') { 75 | Copy-Item $binPath/*.pdb $targetDir 76 | } 77 | 78 | # Copy module manifest, but fix the version to match what we've specified in the binary module. 79 | $moduleManifestContent = ConvertTo-CRLF (Get-Content -Path 'src/Microsoft.WinGet.CommandNotFound.psd1' -Raw) 80 | $versionInfo = (Get-ChildItem -Path $targetDir/Microsoft.WinGet.CommandNotFound.dll).VersionInfo 81 | $version = $versionInfo.FileVersion 82 | 83 | $moduleManifestContent = [regex]::Replace($moduleManifestContent, "ModuleVersion = '.*'", "ModuleVersion = '$version'") 84 | $moduleManifestContent | Set-Content -Path $targetDir/Microsoft.WinGet.CommandNotFound.psd1 85 | 86 | # Make sure we don't ship any read-only files 87 | foreach ($file in (Get-ChildItem -Recurse -File $targetDir)) { 88 | $file.IsReadOnly = $false 89 | } 90 | } 91 | 92 | <# 93 | Synopsis: Zip up the binary for release. 94 | #> 95 | task ZipRelease LayoutModule, { 96 | Compress-Archive -Force -LiteralPath $targetDir -DestinationPath "bin/$Configuration/Microsoft.WinGet.CommandNotFound.zip" 97 | } 98 | 99 | <# 100 | Synopsis: Install newly built Microsoft.WinGet.CommandNotFound 101 | #> 102 | task Install LayoutModule, { 103 | 104 | function Install($InstallDir) { 105 | if (!(Test-Path -Path $InstallDir)) 106 | { 107 | New-Item -ItemType Directory -Force $InstallDir 108 | } 109 | 110 | try 111 | { 112 | if (Test-Path -Path $InstallDir\Microsoft.WinGet.CommandNotFound) 113 | { 114 | Remove-Item -Recurse -Force $InstallDir\Microsoft.WinGet.CommandNotFound -ErrorAction Stop 115 | } 116 | Copy-Item -Recurse $targetDir $InstallDir 117 | } 118 | catch 119 | { 120 | Write-Error -Message "Can't install, module is probably in use." 121 | } 122 | } 123 | 124 | Install "$HOME\Documents\PowerShell\Modules" 125 | } 126 | 127 | <# 128 | Synopsis: Publish to PSGallery 129 | #> 130 | task Publish -If ($Configuration -eq 'Release') { 131 | 132 | $binDir = "$PSScriptRoot/bin/Release/Microsoft.WinGet.CommandNotFound" 133 | 134 | # Check signatures before publishing 135 | Get-ChildItem -Recurse $binDir -Include "*.dll","*.ps*1" | Get-AuthenticodeSignature | ForEach-Object { 136 | if ($_.Status -ne 'Valid') { 137 | throw "$($_.Path) is not signed" 138 | } 139 | if ($_.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation.*') { 140 | throw "$($_.Path) is not signed with a Microsoft signature" 141 | } 142 | } 143 | 144 | # Check newlines in signed files before publishing 145 | Get-ChildItem -Recurse $binDir -Include "*.ps*1" | Get-AuthenticodeSignature | ForEach-Object { 146 | $lines = (Get-Content $_.Path | Measure-Object).Count 147 | $fileBytes = [System.IO.File]::ReadAllBytes($_.Path) 148 | $toMatch = ($fileBytes | ForEach-Object { "{0:X2}" -f $_ }) -join ';' 149 | $crlf = ([regex]::Matches($toMatch, ";0D;0A") | Measure-Object).Count 150 | 151 | if ($lines -ne $crlf) { 152 | throw "$($_.Path) appears to have mixed newlines" 153 | } 154 | } 155 | 156 | $manifest = Import-PowerShellDataFile $binDir/Microsoft.WinGet.CommandNotFound.psd1 157 | 158 | $version = $manifest.ModuleVersion 159 | if ($null -ne $manifest.PrivateData) 160 | { 161 | $psdata = $manifest.PrivateData['PSData'] 162 | if ($null -ne $psdata) 163 | { 164 | $prerelease = $psdata['Prerelease'] 165 | if ($null -ne $prerelease) 166 | { 167 | $version = $version + '-' + $prerelease 168 | } 169 | } 170 | } 171 | 172 | $yes = Read-Host "Publish version $version (y/n)" 173 | 174 | if ($yes -ne 'y') { throw "Publish aborted" } 175 | 176 | $nugetApiKey = Read-Host -AsSecureString "Nuget api key for PSGallery" 177 | 178 | $publishParams = @{ 179 | Path = $binDir 180 | NuGetApiKey = [PSCredential]::new("user", $nugetApiKey).GetNetworkCredential().Password 181 | Repository = "PSGallery" 182 | ProjectUri = 'https://github.com/Microsoft/winget-command-not-found' 183 | } 184 | 185 | Publish-Module @publishParams 186 | } 187 | 188 | <# 189 | Synopsis: Remove temporary items. 190 | #> 191 | task Clean { 192 | git clean -fdx 193 | } 194 | 195 | <# 196 | Synopsis: Default build rule - build and create module layout 197 | #> 198 | task . LayoutModule 199 | -------------------------------------------------------------------------------- /src/WinGetCommandNotFoundFeedbackPredictor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation 2 | // The Microsoft Corporation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections; 6 | using System.Collections.ObjectModel; 7 | using System.Globalization; 8 | using System.Management.Automation; 9 | using System.Management.Automation.Subsystem; 10 | using System.Management.Automation.Subsystem.Feedback; 11 | using System.Management.Automation.Subsystem.Prediction; 12 | using Microsoft.Extensions.ObjectPool; 13 | 14 | namespace Microsoft.WinGet.CommandNotFound 15 | { 16 | public sealed class WinGetCommandNotFoundFeedbackPredictor : IFeedbackProvider, ICommandPredictor, IModuleAssemblyInitializer, IModuleAssemblyCleanup 17 | { 18 | private readonly Guid _guid = new Guid("09cd038b-a75f-4d91-8f71-f29e1ab480dc"); 19 | 20 | private readonly ObjectPool _pool; 21 | 22 | private const int _maxSuggestions = 20; 23 | 24 | private List _candidates = new List(); 25 | 26 | private bool _warmedUp; 27 | 28 | private WinGetCommandNotFoundFeedbackPredictor() 29 | { 30 | var provider = new DefaultObjectPoolProvider(); 31 | _pool = provider.Create(new PooledPowerShellObjectPolicy()); 32 | _pool.Return(_pool.Get()); 33 | } 34 | 35 | public Guid Id => _guid; 36 | 37 | public string Name => "Windows Package Manager - WinGet"; 38 | 39 | public string Description => "Finds missing commands that can be installed via WinGet."; 40 | 41 | public Dictionary? FunctionsToDefine => null; 42 | 43 | private async void WarmUp() 44 | { 45 | var ps = _pool.Get(); 46 | try 47 | { 48 | await ps.AddCommand("Find-WinGetPackage") 49 | .AddParameter("Count", 1) 50 | .InvokeAsync(); 51 | } 52 | catch (Exception /*ex*/) {} 53 | finally 54 | { 55 | _pool.Return(ps); 56 | _warmedUp = true; 57 | } 58 | } 59 | 60 | public void OnImport() 61 | { 62 | if (!Platform.IsWindows || !IsWinGetInstalled()) 63 | { 64 | return; 65 | } 66 | 67 | WarmUp(); 68 | 69 | SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, this); 70 | SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, this); 71 | } 72 | 73 | public void OnRemove(PSModuleInfo psModuleInfo) 74 | { 75 | if (!Platform.IsWindows || !IsWinGetInstalled()) 76 | { 77 | return; 78 | } 79 | 80 | SubsystemManager.UnregisterSubsystem(Id); 81 | SubsystemManager.UnregisterSubsystem(Id); 82 | } 83 | 84 | private bool IsWinGetInstalled() 85 | { 86 | // Ensure WinGet is installed 87 | using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) 88 | { 89 | var results = pwsh.AddCommand("Get-Command") 90 | .AddParameter("Name", "winget") 91 | .Invoke(); 92 | 93 | if (results.Count is 0) 94 | { 95 | return false; 96 | } 97 | } 98 | 99 | return true; 100 | } 101 | 102 | /// 103 | /// Gets feedback based on the given commandline and error record. 104 | /// 105 | public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token) 106 | { 107 | var target = (string)context.LastError!.TargetObject; 108 | if (target is null) 109 | { 110 | return null; 111 | } 112 | 113 | try 114 | { 115 | bool tooManySuggestions = false; 116 | string packageMatchFilterField = "command"; 117 | var pkgList = FindPackages(target, ref tooManySuggestions, ref packageMatchFilterField); 118 | if (pkgList.Count == 0) 119 | { 120 | return null; 121 | } 122 | 123 | // Build list of suggestions 124 | _candidates.Clear(); 125 | foreach (var pkg in pkgList) 126 | { 127 | _candidates.Add(string.Format(CultureInfo.InvariantCulture, "winget install --id {0}", pkg.Members["Id"].Value.ToString())); 128 | } 129 | 130 | // Build footer message 131 | var footerMessage = tooManySuggestions ? 132 | string.Format(CultureInfo.InvariantCulture, "Additional results can be found using \"winget search --{0} {1}\"", packageMatchFilterField, target) : 133 | null; 134 | 135 | return new FeedbackItem( 136 | "Try installing this package using winget:", 137 | _candidates, 138 | footerMessage, 139 | FeedbackDisplayLayout.Portrait); 140 | } 141 | catch (Exception /*ex*/) 142 | { 143 | return new FeedbackItem($"Failed to execute WinGet Command Not Found.{Environment.NewLine}This is a known issue if PowerShell 7 is installed from the Store or MSIX (see https://github.com/microsoft/winget-command-not-found/issues/3). If that isn't your case, please report an issue.", new List(), FeedbackDisplayLayout.Portrait); 144 | } 145 | } 146 | 147 | private Collection FindPackages(string query, ref bool tooManySuggestions, ref string packageMatchFilterField) 148 | { 149 | if (!_warmedUp) 150 | { 151 | // Given that the warm-up was not done, it's no good to carry on because we 152 | // will likely get a newly created PowerShell object 153 | // and pay the same overhead of the warmup method. 154 | return new Collection(); 155 | } 156 | 157 | var ps = _pool.Get(); 158 | try 159 | { 160 | var common = new Hashtable() 161 | { 162 | ["Source"] = "winget", 163 | }; 164 | 165 | // 1) Search by command 166 | var pkgList = ps.AddCommand("Find-WinGetPackage") 167 | .AddParameter("Command", query) 168 | .AddParameter("MatchOption", "StartsWithCaseInsensitive") 169 | .AddParameters(common) 170 | .Invoke(); 171 | if (pkgList.Count > 0) 172 | { 173 | tooManySuggestions = pkgList.Count > _maxSuggestions; 174 | packageMatchFilterField = "command"; 175 | return pkgList; 176 | } 177 | 178 | // 2) No matches found, 179 | // search by name 180 | ps.Commands.Clear(); 181 | pkgList = ps.AddCommand("Find-WinGetPackage") 182 | .AddParameter("Name", query) 183 | .AddParameter("MatchOption", "ContainsCaseInsensitive") 184 | .AddParameters(common) 185 | .Invoke(); 186 | if (pkgList.Count > 0) 187 | { 188 | tooManySuggestions = pkgList.Count > _maxSuggestions; 189 | packageMatchFilterField = "name"; 190 | return pkgList; 191 | } 192 | 193 | // 3) No matches found, 194 | // search by moniker 195 | ps.Commands.Clear(); 196 | pkgList = ps.AddCommand("Find-WinGetPackage") 197 | .AddParameter("Moniker", query) 198 | .AddParameter("MatchOption", "ContainsCaseInsensitive") 199 | .AddParameters(common) 200 | .Invoke(); 201 | tooManySuggestions = pkgList.Count > _maxSuggestions; 202 | packageMatchFilterField = "moniker"; 203 | return pkgList; 204 | } 205 | finally 206 | { 207 | _pool.Return(ps); 208 | } 209 | } 210 | 211 | public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback) 212 | { 213 | return feedback switch 214 | { 215 | PredictorFeedbackKind.CommandLineAccepted => true, 216 | _ => false, 217 | }; 218 | } 219 | 220 | public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken) 221 | { 222 | if (_candidates.Count() > 0) 223 | { 224 | string input = context.InputAst.Extent.Text; 225 | List? result = null; 226 | 227 | foreach (string c in _candidates) 228 | { 229 | if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase)) 230 | { 231 | result ??= new List(_candidates.Count); 232 | result.Add(new PredictiveSuggestion(c)); 233 | } 234 | } 235 | 236 | if (result is not null) 237 | { 238 | return new SuggestionPackage(result); 239 | } 240 | } 241 | 242 | return default; 243 | } 244 | 245 | public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) 246 | { 247 | // Reset the candidate state. 248 | _candidates.Clear(); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /.pipelines/releaseBuild.yml: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # OneBranch Pipelines # 3 | # This pipeline was created by EasyStart from a sample located at: # 4 | # https://aka.ms/obpipelines/easystart/samples # 5 | # Documentation: https://aka.ms/obpipelines # 6 | # Yaml Schema: https://aka.ms/obpipelines/yaml/schema # 7 | # Retail Tasks: https://aka.ms/obpipelines/tasks # 8 | # Support: https://aka.ms/onebranchsup # 9 | ################################################################################# 10 | 11 | name: Microsoft.WinGet.CommandNotFound-ModuleBuild-$(Build.BuildId) 12 | trigger: none 13 | pr: none 14 | 15 | variables: 16 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 17 | POWERSHELL_TELEMETRY_OPTOUT: 1 18 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 19 | WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest 20 | 21 | resources: 22 | repositories: 23 | - repository: onebranchTemplates 24 | type: git 25 | name: OneBranch.Pipelines/GovernedTemplates 26 | ref: refs/heads/main 27 | 28 | extends: 29 | template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates 30 | parameters: 31 | featureFlags: 32 | WindowsHostVersion: '1ESWindows2022' 33 | globalSdl: 34 | disableLegacyManifest: true 35 | sbom: 36 | enabled: true 37 | packageName: Microsoft.WinGet.CommandNotFound 38 | codeql: 39 | compiled: 40 | enabled: true 41 | asyncSdl: # https://aka.ms/obpipelines/asyncsdl 42 | enabled: true 43 | forStages: [Build] 44 | credscan: 45 | enabled: true 46 | scanFolder: $(Build.SourcesDirectory)\Microsoft.WinGet.CommandNotFound\src 47 | binskim: 48 | enabled: true 49 | apiscan: 50 | enabled: false 51 | 52 | stages: 53 | - stage: buildstage 54 | displayName: Build and Sign Microsoft.WinGet.CommandNotFound 55 | jobs: 56 | - job: buildjob 57 | displayName: Build Microsoft.WinGet.CommandNotFound Files 58 | variables: 59 | - name: ob_outputDirectory 60 | value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' 61 | - name: repoRoot 62 | value: $(Build.SourcesDirectory) 63 | - name: ob_sdl_tsa_configFile 64 | value: $(repoRoot)\.config\tsaoptions.json 65 | - name: signSrcPath 66 | value: $(repoRoot)\bin\Release\Microsoft.WinGet.CommandNotFound 67 | - name: ob_sdl_sbom_enabled 68 | value: true 69 | - name: ob_signing_setup_enabled 70 | value: true 71 | #CodeQL tasks added manually to workaround signing failures 72 | - name: ob_sdl_codeql_compiled_enabled 73 | value: false 74 | 75 | pool: 76 | type: windows 77 | steps: 78 | - pwsh: | 79 | if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { 80 | throw "tsaoptions.json does not exist under $(repoRoot)/.config" 81 | } 82 | displayName: Test if tsaoptions.json exists 83 | env: 84 | # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. 85 | ob_restore_phase: true 86 | 87 | - pwsh: | 88 | Write-Host "PS Version: $($PSVersionTable.PSVersion)" 89 | Set-Location -Path '$(repoRoot)' 90 | .\build.ps1 -Bootstrap 91 | displayName: Bootstrap 92 | env: 93 | # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. 94 | ob_restore_phase: true 95 | 96 | # Add CodeQL Init task right before your 'Build' step. 97 | - task: CodeQL3000Init@0 98 | env: 99 | # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. 100 | ob_restore_phase: true 101 | inputs: 102 | Enabled: true 103 | AnalyzeInPipeline: true 104 | Language: csharp 105 | 106 | - pwsh: | 107 | Write-Host "PS Version: $($($PSVersionTable.PSVersion))" 108 | Set-Location -Path '$(repoRoot)' 109 | .\build.ps1 -Configuration Release -Framework net8.0 110 | displayName: Build 111 | env: 112 | # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. 113 | ob_restore_phase: true 114 | 115 | # Add CodeQL Finalize task right after your 'Build' step. 116 | - task: CodeQL3000Finalize@0 117 | condition: always() 118 | env: 119 | # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. 120 | ob_restore_phase: true 121 | 122 | - task: onebranch.pipeline.signing@1 123 | displayName: Sign 1st party files 124 | inputs: 125 | command: 'sign' 126 | signing_profile: external_distribution 127 | files_to_sign: '*.psd1;*.psm1;*.ps1;*.ps1xml;**\Microsoft*.dll;Microsoft.WinGet.CommandNotFound.dll' 128 | search_root: $(signSrcPath) 129 | 130 | # Verify the signatures 131 | - pwsh: | 132 | $HasInvalidFiles = $false 133 | $WrongCert = @{} 134 | Get-ChildItem -Path $(signSrcPath) -Recurse -Include "*.dll","*.ps*1*" | ` 135 | Get-AuthenticodeSignature | ForEach-Object { 136 | Write-Host "$($_.Path): $($_.Status)" 137 | if ($_.Status -ne 'Valid') { $HasInvalidFiles = $true } 138 | if ($_.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation.*') { 139 | $WrongCert.Add($_.Path, $_.SignerCertificate.Subject) 140 | } 141 | } 142 | 143 | if ($HasInvalidFiles) { throw "Authenticode verification failed. There is one or more invalid files." } 144 | if ($WrongCert.Count -gt 0) { 145 | $WrongCert 146 | throw "Certificate should have the subject starts with 'Microsoft Corporation'" 147 | } 148 | 149 | Write-Host "Display files in the folder ..." -ForegroundColor Yellow 150 | Get-ChildItem -Path $(signSrcPath) -Recurse | Out-String -Width 120 151 | displayName: 'Verify the signed files' 152 | 153 | - task: CopyFiles@2 154 | displayName: "Copy signed files to ob_outputDirectory - '$(ob_outputDirectory)'" 155 | inputs: 156 | SourceFolder: $(signSrcPath) 157 | Contents: '**\*' 158 | TargetFolder: $(ob_outputDirectory) 159 | 160 | - pwsh: | 161 | $versionInfo = Get-Item "$(signSrcPath)\Microsoft.WinGet.CommandNotFound.dll" | Select-Object -Expand VersionInfo 162 | $moduleVersion = $versionInfo.ProductVersion.Split('+')[0] 163 | $vstsCommandString = "vso[task.setvariable variable=ob_sdl_sbom_packageversion]${moduleVersion}" 164 | 165 | Write-Host "sending $vstsCommandString" 166 | Write-Host "##$vstsCommandString" 167 | displayName: Setup SBOM Package Version 168 | 169 | - job: nupkg 170 | dependsOn: buildjob 171 | displayName: Package Microsoft.WinGet.CommandNotFound module 172 | variables: 173 | - name: ob_outputDirectory 174 | value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' 175 | - name: repoRoot 176 | value: $(Build.SourcesDirectory) 177 | - name: ob_sdl_tsa_configFile 178 | value: $(repoRoot)\.config\tsaoptions.json 179 | # Disable because SBOM was already built in the previous job 180 | - name: ob_sdl_sbom_enabled 181 | value: false 182 | - name: signOutPath 183 | value: $(repoRoot)\signed\Microsoft.WinGet.CommandNotFound 184 | - name: nugetPath 185 | value: $(repoRoot)\signed\NuGetPackage 186 | - name: ob_signing_setup_enabled 187 | value: true 188 | # This job is not compiling code, so disable codeQL 189 | - name: ob_sdl_codeql_compiled_enabled 190 | value: false 191 | 192 | pool: 193 | type: windows 194 | steps: 195 | - task: DownloadPipelineArtifact@2 196 | displayName: 'Download build files' 197 | inputs: 198 | targetPath: $(signOutPath) 199 | artifact: drop_buildstage_buildjob 200 | 201 | - pwsh: | 202 | Get-ChildItem $(signOutPath) -Recurse 203 | New-Item -Path $(nugetPath) -ItemType Directory > $null 204 | displayName: Capture artifacts structure 205 | 206 | - pwsh: | 207 | try { 208 | Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force 209 | $RepoName = "WCNFLocal" 210 | mkdir $(nugetPath) -ErrorAction SilentlyContinue 211 | Register-PSResourceRepository -Name $RepoName -Uri $(nugetPath) -Trusted 212 | # -SkipModuleManifestValidate: module needs Pwsh 7.4 but machine is on an earlier version 213 | # -SkipDependenciesCheck: module depends on Microsoft.WinGet.Client which will definitely be available on PSGallery 214 | Publish-PSResource -Verbose -Path $(signOutPath) -Repository $RepoName -SkipModuleManifestValidate -SkipDependenciesCheck 215 | } finally { 216 | Unregister-PSRepository -Name $RepoName -ErrorAction SilentlyContinue 217 | } 218 | Get-ChildItem -Path $(nugetPath) 219 | displayName: 'Create the NuGet package' 220 | 221 | - task: onebranch.pipeline.signing@1 222 | displayName: Sign nupkg 223 | inputs: 224 | command: 'sign' 225 | signing_profile: external_distribution 226 | files_to_sign: '*.nupkg' 227 | search_root: $(nugetPath) 228 | 229 | - task: CopyFiles@2 230 | displayName: "Copy nupkg to ob_outputDirectory - '$(ob_outputDirectory)'" 231 | inputs: 232 | SourceFolder: $(nugetPath) 233 | Contents: '**\*' 234 | TargetFolder: $(ob_outputDirectory) 235 | --------------------------------------------------------------------------------