├── 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 | 
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 |
--------------------------------------------------------------------------------