├── .gitignore ├── src ├── Microsoft.PowerShell.PSAdapter.psd1 ├── Microsoft.PowerShell.PSAdapter.csproj ├── JsonCompletion.cs └── Microsoft.PowerShell.PSAdapter.cs ├── .github ├── CODE_OF_CONDUCT.md └── SECURITY.md ├── test ├── PSAdapter.Subsystem.Tests.ps1 └── PSAdapter.Suggestion.Tests.ps1 ├── tools └── install-preview-linux.ps1 ├── LICENSE ├── README.md └── yaml └── releaseBuild.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /staging 3 | /src/bin 4 | /src/obj 5 | *.nupkg 6 | -------------------------------------------------------------------------------- /src/Microsoft.PowerShell.PSAdapter.psd1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | @{ 5 | ModuleVersion = '0.3.0' 6 | GUID = '6edf7436-db79-4b5b-b889-4e6d6a1c8680' 7 | Author = 'PowerShell Team' 8 | CompanyName = "Microsoft Corporation" 9 | Copyright = "Copyright © Microsoft Corporation." 10 | Description = 'Enable suggestions for adding a PS adapter if found.' 11 | PowerShellVersion = '7.4' 12 | NestedModules = @('Microsoft.PowerShell.PSAdapter.dll') 13 | } 14 | -------------------------------------------------------------------------------- /.github/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 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) 11 | -------------------------------------------------------------------------------- /test/PSAdapter.Subsystem.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Subsystem tests" { 5 | BeforeAll { 6 | $subsystemResults = Get-PSSubsystem 7 | } 8 | 9 | It "The CommandPredictor should include the PSAdapter" { 10 | $subsystemResults.where({$_.kind -eq "CommandPredictor"}).Implementations.Name | Should -Contain "PSAdapter" 11 | } 12 | 13 | It "The FeedbackProvider should include the PSAdapter" { 14 | $subsystemResults.where({$_.kind -eq "FeedbackProvider"}).Implementations.Name | Should -Contain "PSAdapter" 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tools/install-preview-linux.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # Install preview on ubuntu 5 | # Update the list of packages 6 | write-progress "apt update" -perc 10 7 | apt -qq update 8 | # Install pre-requisite packages. 9 | write-progress "apt install vim wget https common" -perc 20 10 | $null = apt -qq install -y vim wget apt-transport-https software-properties-common 2>&1 11 | # Download the Microsoft repository GPG keys 12 | write-progress "wget" -perc 40 13 | wget -q "https://packages.microsoft.com/config/ubuntu/$(/usr/bin/lsb_release -rs)/packages-microsoft-prod.deb" 14 | # Register the Microsoft repository GPG keys 15 | write-progress "register repository GPG keys" -perc 50 16 | dpkg -i packages-microsoft-prod.deb 17 | # Delete the the Microsoft repository GPG keys file 18 | rm packages-microsoft-prod.deb 19 | # Update the list of packages after we added packages.microsoft.com 20 | write-progress "apt update again" -perc 70 21 | apt -qq update 22 | # Install PowerShell 23 | write-progress "apt install powershell-preview" -perc 90 24 | $null = apt -qq install -y powershell-preview 2>&1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 PowerShell Team 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 | -------------------------------------------------------------------------------- /src/Microsoft.PowerShell.PSAdapter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | enable 7 | enable 8 | true 9 | true 10 | any 11 | 12 | 13 | 14 | 15 | true 16 | Portable 17 | 18 | 19 | 20 | 21 | false 22 | None 23 | 24 | 25 | 26 | 27 | contentFiles 28 | All 29 | 30 | 31 | 32 | 33 | 34 | PreserveNewest 35 | PreserveNewest 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSAdapter 2 | 3 | This is a FeedbackProvider and SuggestionPredictor for native utilities which 4 | have an adapter written for them, or if the `jc` utility is installed suggestions 5 | on how it may be incorporated into the users command line. 6 | 7 | The following is a transcript where the `uname` command is used and has a 8 | `uname-adapter` script which can convert the output to an object, as well as 9 | how `jc` can be used to transform the text output into an object suitable 10 | for use with PowerShell. 11 | 12 | This module will work only with PowerShell 7.4 preview 3 or newer. 13 | 14 | ```powershell 15 | PS> ^C 16 | PS> pwsh-preview 17 | PS> import-module Microsoft.PowerShell.PSAdapter 18 | PS> set-psReadLineOption -PredictionViewStyle ListView 19 | PS> uname -a 20 | > uname -a [History] 21 | > uname | jc --uname | ConvertFrom-Json [PSAdapter] 22 | > uname | uname-adapter [PSAdapter] 23 | Darwin JamesiMac20.local 22.5.0 Darwin Kernel Version 22.5.0: Thu Jun 8 22:22:22 PDT 2023; root:xnu-8796.121.3~7/RELEASE_X86_64 x86_64 24 | 25 | [PSAdapter] 26 | PSAdapter found additional ways to run. 27 | ➤ uname -a | jc --uname | ConvertFrom-Json 28 | ➤ uname -a | uname-adapter 29 | 30 | PS/PSAdapter> uname -a | jc --uname | ConvertFrom-Json 31 | 32 | machine : x86_64 33 | kernel_name : Darwin 34 | node_name : JamesiMac20.local 35 | kernel_release : 22.5.0 36 | kernel_version : Darwin Kernel Version 22.5.0: Thu Jun 8 22:22:22 PDT 2023; root:xnu-8796.121.3~7/RELEASE_X86_64 37 | 38 | PS> 39 | ``` 40 | 41 | ## Code of Conduct 42 | 43 | Please see our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before participating in this project. 44 | 45 | ## Security Policy 46 | 47 | For any security issues, please see our [Security Policy](.github/SECURITY.md). 48 | -------------------------------------------------------------------------------- /test/PSAdapter.Suggestion.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | Describe "Suggestion Tests" { 5 | BeforeAll { 6 | # this uses reflection to get at the generator 7 | $singleton = [PSAdapterProvider.PSAdapterFeedbackPredictor]::Singleton 8 | $generator = $singleton.gettype().getfield("_suggestionGenerator", "NonPublic,Instance").GetValue($singleton) 9 | # arp seems to be a binary on both windows and *nix 10 | $Ast1 = { arp }.Ast.Find({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) 11 | $Ast2 = { arp | jc --arp | ConvertFrom-Json }.Ast.Find({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) 12 | $Ast3 = { arp | arp-adapter }.Ast.Find({$args[0] -is [System.Management.Automation.language.CommandAst]}, $false) 13 | $Ast4 = { arp -abc | jc --arp }.Ast.Find({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) 14 | 15 | $savedPath = $env:PATH 16 | $env:PATH += "$([io.Path]::PathSeparator)$TESTDRIVE" 17 | '"output"' > "${TESTDRIVE}/arp-adapter.ps1" 18 | 19 | # the first call will always miss the cache, so call it here 20 | # it is also timing sensitive (because we are hitting the file system for the script adapter), so add a sleep 21 | $generator.GetSuggestions($Ast1) | Out-Null 22 | start-sleep 2 23 | $generator.GetSuggestions($Ast1) | Out-Null 24 | } 25 | 26 | AfterAll { 27 | $env:PATH = $savedPath 28 | } 29 | 30 | It "Should provide a jc suggestion for '$ast1'" { 31 | $generator.GetSuggestions($Ast1) | Should -Contain "arp | jc --arp | ConvertFrom-Json" 32 | } 33 | 34 | It "Should not provide a jc suggestion for '$ast2'" { 35 | $singleton.GetFilteredSuggestions($Ast2) | Should -BeNullOrEmpty 36 | } 37 | 38 | It "Should provide an arp-adapter suggestion for '$ast3'" { 39 | $suggestions = $generator.GetSuggestions($Ast3) 40 | $suggestions | Should -Contain "arp | arp-adapter" 41 | } 42 | 43 | It "Should preserve the parameters of '$Ast4'" { 44 | $suggestions = $generator.GetSuggestions($Ast4) 45 | $matches = $suggestions | Where-Object { $_ -match "abc" } 46 | $matches.Count | Should -Be 2 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /.github/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), [Xamarin](https://github.com/xamarin) and [PowerShell](https://github.com/PowerShell). 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 | -------------------------------------------------------------------------------- /src/JsonCompletion.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Management.Automation; 9 | using System.Management.Automation.Language; 10 | using System.Threading.Tasks; 11 | 12 | namespace PSAdapterProvider 13 | { 14 | public class SuggestionGenerator 15 | { 16 | private PowerShell _ps; 17 | private CommandInvocationIntrinsics _cIntrinsics; 18 | private ConcurrentDictionary _commandAdapterCache { get; set; } 19 | private ConcurrentBag _nativeCommandCache { get; set; } 20 | private CommandTypes allowedAdapterTypes = CommandTypes.Application | CommandTypes.ExternalScript | CommandTypes.Script; 21 | private bool hasJcCommand = false; 22 | // supported commands for jc 23 | private readonly HashSet _jcCommands = new HashSet(StringComparer.OrdinalIgnoreCase) { 24 | "arp", "cksum", "crontab", "date", "df", "dig", "dir", "du", "file", "finger", 25 | "free", "hash", "id", "ifconfig", "iostat", "jobs", "lsof", "mount", "mpstat", 26 | "netstat", "route", "stat", "sysctl", "traceroute", "uname", "uptime", "w", "wc", 27 | "who", "zipinfo" 28 | }; 29 | 30 | public SuggestionGenerator() 31 | { 32 | _ps = PowerShell.Create(RunspaceMode.NewRunspace); 33 | _cIntrinsics = _ps.Runspace.SessionStateProxy.InvokeCommand; 34 | hasJcCommand = CheckJc(); 35 | _commandAdapterCache = new ConcurrentDictionary(); 36 | _nativeCommandCache = new ConcurrentBag(); 37 | } 38 | 39 | // This both returns and sets the value of hasJcCommand 40 | // It can be used to check if the jc command is available after the constructor has been called 41 | public bool CheckJc(bool? defaultValue = null) 42 | { 43 | if (defaultValue.HasValue) 44 | { 45 | hasJcCommand = defaultValue.Value; 46 | return hasJcCommand; 47 | } 48 | 49 | if(null != _cIntrinsics.GetCommand("jc", CommandTypes.Application)) 50 | { 51 | hasJcCommand = true; 52 | return true; 53 | } 54 | hasJcCommand = false; 55 | return false; 56 | } 57 | 58 | public void Dispose() 59 | { 60 | _ps.Dispose(); 61 | } 62 | 63 | private void TryAddJcAdapter(string commandName) 64 | { 65 | if (_jcCommands.TryGetValue(commandName, out string? adapterCommand)) 66 | { 67 | _commandAdapterCache.TryAdd("jc:" + commandName, "jc --" + adapterCommand + " | ConvertFrom-Json"); 68 | } 69 | } 70 | 71 | private void TryAddJsonAdapter(string commandName) 72 | { 73 | var jsonAdapter = commandName + "-adapter"; 74 | var cmdInfo = _cIntrinsics.GetCommand(jsonAdapter, allowedAdapterTypes); 75 | if (null != cmdInfo) 76 | { 77 | _commandAdapterCache.TryAdd("json:" + commandName, jsonAdapter); 78 | } 79 | } 80 | 81 | public void ClearAdapterCache() 82 | { 83 | _commandAdapterCache.Clear(); 84 | } 85 | 86 | // this is public for testing purposes 87 | // We don't really have time to find the adapter, so if it's not in the cache, we'll return an empty list. 88 | // We should find something the second time around, if it exists 89 | public List? GetSuggestions(CommandAst cAst) 90 | { 91 | List suggestions = new List(); 92 | string commandName = cAst.GetCommandName(); 93 | if (null == commandName) 94 | { 95 | return null; 96 | } 97 | 98 | var commandWithoutExtension = Path.GetFileNameWithoutExtension(commandName); 99 | 100 | // only return suggestions on external scripts or applications 101 | // check the cache first to see if we've already found the command 102 | // we may get cancelled before we complete the following checks, but we should be populating the caches. 103 | if(! _nativeCommandCache.Contains(commandWithoutExtension)) 104 | { 105 | var cmdInfo = _cIntrinsics.GetCommand(commandWithoutExtension, CommandTypes.ExternalScript | CommandTypes.Application); 106 | if (cmdInfo == null) 107 | { 108 | return null; 109 | } 110 | _nativeCommandCache.Add(commandWithoutExtension); 111 | } 112 | 113 | string? adapter; 114 | if (hasJcCommand && _commandAdapterCache.TryGetValue("jc:" + commandWithoutExtension, out adapter)) 115 | { 116 | suggestions.Add(cAst.Extent.Text + " | " + adapter); 117 | } 118 | else 119 | { 120 | Task.Run(() => TryAddJcAdapter(commandWithoutExtension)); 121 | // TryAddJcAdapter(commandWithoutExtension); 122 | } 123 | 124 | // we need to check if the command has an adapter with the shape -adapter.* 125 | var jsonAdapter = commandWithoutExtension + "-adapter"; 126 | if (_commandAdapterCache.TryGetValue("json:" + commandName, out adapter)) 127 | { 128 | suggestions.Add(cAst.Extent.Text + " | " + adapter); 129 | } 130 | else 131 | { 132 | Task.Run(() => TryAddJsonAdapter(commandWithoutExtension)); 133 | // TryAddJsonAdapter(commandWithoutExtension); 134 | } 135 | 136 | return suggestions; 137 | } 138 | 139 | public List GetSuggestedPipelines(CommandAst cAst) 140 | { 141 | List pipelines = new List(); 142 | 143 | foreach(string suggestion in GetSuggestions(cAst) ?? new List()) 144 | { 145 | PipelineAst? pAst = Parser.ParseInput(suggestion, out _, out ParseError[] errors).Find(ast => ast is PipelineAst, false) as PipelineAst; 146 | if (errors.Length == 0 && null != pAst) 147 | { 148 | pipelines.Add(pAst); 149 | } 150 | } 151 | 152 | return pipelines; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /yaml/releaseBuild.yml: -------------------------------------------------------------------------------- 1 | # release build 2 | name: PSAdapter-Release-$(Date:yyyyMMdd)$(Rev:.rr) 3 | trigger: none 4 | 5 | pr: none 6 | 7 | variables: 8 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 9 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 10 | POWERSHELL_TELEMETRY_OPTOUT: 1 11 | 12 | resources: 13 | repositories: 14 | - repository: ComplianceRepo 15 | type: github 16 | endpoint: ComplianceGHRepo 17 | name: PowerShell/compliance 18 | ref: master 19 | 20 | stages: 21 | - stage: BuildAndSign 22 | displayName: Build and Sign 23 | pool: 24 | name: PowerShell1ES 25 | demands: 26 | - ImageOverride -equals PSMMS2019-Preview-Secure 27 | jobs: 28 | - job: 'BuildAndSign' 29 | displayName: Build and Sign 30 | variables: 31 | - group: ESRP 32 | steps: 33 | - checkout: self 34 | 35 | - task: UseDotNet@2 36 | displayName: 'Use .NET Core sdk 2.1' 37 | inputs: 38 | packageType: sdk 39 | includePreviewVersions: true 40 | version: 2.1.x 41 | 42 | - task: UseDotNet@2 43 | displayName: 'Use .NET Core sdk 8.x' 44 | inputs: 45 | packageType: sdk 46 | includePreviewVersions: true 47 | version: 8.x 48 | 49 | - pwsh: | 50 | Get-ChildItem -Path env: 51 | displayName: Capture environment 52 | condition: succeededOrFailed() 53 | 54 | - pwsh: | 55 | Set-Location "$(Build.SourcesDirectory)/JsonAdapterPredictor" 56 | Get-ChildItem -Recurse -File -Name | Write-Verbose -Verbose 57 | ./build.ps1 -Release 58 | Get-ChildItem -Recurse -File -Name | Write-Verbose -Verbose 59 | 60 | displayName: Execute Build 61 | 62 | - pwsh: | 63 | $signSrcPath = "$(Build.SourcesDirectory)/JsonAdapterPredictor/out" 64 | # Set signing src path variable 65 | $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" 66 | Write-Host ("sending " + $vstsCommandString) 67 | Write-Host "##$vstsCommandString" 68 | 69 | $signOutPath = "$(Build.SourcesDirectory)/JsonAdapterPredictor/signed" 70 | $null = New-Item -ItemType Directory -Path $signOutPath 71 | # Set signing out path variable 72 | $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" 73 | Write-Host "sending " + $vstsCommandString 74 | Write-Host "##$vstsCommandString" 75 | 76 | # Set path variable for guardian codesign validation 77 | $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" 78 | Write-Host "sending " + $vstsCommandString 79 | Write-Host "##$vstsCommandString" 80 | 81 | $packageVersion = (Import-PowerShellDataFile $(Build.SourcesDirectory)/JsonAdapterPredictor/src/Microsoft.PowerShell.PSAdapter.psd1).ModuleVersion 82 | $vstsCommandString = "vso[task.setvariable variable=PackageVersion]$packageVersion" 83 | Write-Host ("sending " + $vstsCommandString) 84 | Write-Host "##$vstsCommandString" 85 | 86 | displayName: Setup variables for signing 87 | 88 | - publish: "$(Build.SourcesDirectory)/JsonAdapterPredictor/out/" 89 | artifact: out 90 | 91 | displayName: Publish build module files 92 | 93 | - checkout: ComplianceRepo 94 | 95 | - template: EsrpSign.yml@ComplianceRepo 96 | parameters: 97 | # the folder which contains the binaries to sign 98 | buildOutputPath: $(signSrcPath) 99 | # the location to put the signed output 100 | signOutputPath: $(signOutPath) 101 | # the certificate ID to use 102 | certificateId: "CP-230012" 103 | # The file pattern to use 104 | # If not using minimatch: comma separated, with * supported 105 | # If using minimatch: newline separated, with !, **, and * supported. 106 | # See link in the useMinimatch comments. 107 | pattern: '*.dll,*.psd1,*.psm1,*.ps1xml' 108 | # decides if the task should use minimatch for the pattern matching. 109 | # https://github.com/isaacs/minimatch#features 110 | useMinimatch: false 111 | 112 | - template: Sbom.yml@ComplianceRepo 113 | parameters: 114 | BuildDropPath: $(Build.SourcesDirectory)/JsonAdapterPredictor/signed/Microsoft.PowerShell.PSAdapter/$(PackageVersion) 115 | Build_Repository_Uri: 'https://github.com/powershell/textutility' 116 | PackageName: 'Microsoft.PowerShell.PSAdapterPredictor' 117 | PackageVersion: $(PackageVersion) 118 | 119 | - pwsh: | 120 | Set-Location $(Build.SourcesDirectory)/JsonAdapterPredictor 121 | Get-ChildItem -Rec -File | Format-Table LastWriteTime,FullName | Out-String -Str | Write-Verbose -verbose 122 | # packaging this module requires a 7.4 preview 123 | pwsh-preview -c ./build.ps1 -package -NoBuild -UseSignedFiles 124 | 125 | displayName: Package Module 126 | 127 | - pwsh: | 128 | New-Item -Path $(Build.SourcesDirectory)/JsonAdapterPredictor/SignedZip -ItemType Directory -ErrorAction Ignore 129 | Compress-Archive -Path $(Build.SourcesDirectory)/JsonAdapterPredictor/signed/Microsoft.PowerShell.PSAdapter -DestinationPath $(Build.SourcesDirectory)/JsonAdapterPredictor/SignedZip/Microsoft.PowerShell.PSAdapter.zip -Force 130 | displayName: 'Compress archive' 131 | condition: succeededOrFailed() 132 | 133 | - task: PublishPipelineArtifact@1 134 | inputs: 135 | targetpath: $(Build.SourcesDirectory)/JsonAdapterPredictor/staging 136 | artifactName: Staging 137 | 138 | - task: PublishPipelineArtifact@1 139 | inputs: 140 | targetpath: $(Build.SourcesDirectory)/JsonAdapterPredictor/signed/Microsoft.PowerShell.PSAdapter 141 | artifactName: Signed 142 | 143 | - task: PublishPipelineArtifact@1 144 | inputs: 145 | targetpath: $(Build.SourcesDirectory)/JsonAdapterPredictor/SignedZip 146 | artifactName: SignedZip 147 | 148 | - stage: compliance 149 | displayName: Compliance 150 | dependsOn: BuildAndSign 151 | jobs: 152 | - job: Compliance_Job 153 | pool: 154 | name: PowerShell1ES 155 | demands: 156 | - ImageOverride -equals PSMMS2019-Secure 157 | steps: 158 | - checkout: self 159 | - checkout: ComplianceRepo 160 | - download: current 161 | artifact: Signed 162 | 163 | - pwsh: | 164 | Get-ChildItem -Path "$(Pipeline.Workspace)\Signed" -Recurse 165 | displayName: Capture downloaded artifacts 166 | 167 | - template: assembly-module-compliance.yml@ComplianceRepo 168 | parameters: 169 | # binskim 170 | AnalyzeTarget: '$(Pipeline.Workspace)\*.dll' 171 | AnalyzeSymPath: 'SRV*' 172 | # component-governance 173 | sourceScanPath: '$(Build.SourcesDirectory)' 174 | # credscan 175 | suppressionsFile: '' 176 | # TermCheck 177 | optionsRulesDBPath: '' 178 | optionsFTPath: '' 179 | # tsa-upload 180 | codeBaseName: 'textutility_202305' 181 | # selections 182 | APIScan: false # set to false when not using Windows APIs. 183 | -------------------------------------------------------------------------------- /src/Microsoft.PowerShell.PSAdapter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Collections.ObjectModel; 7 | using System.IO; 8 | using System.Management.Automation; 9 | using System.Collections.Concurrent; 10 | using System.Management.Automation.Language; 11 | using System.Management.Automation.Runspaces; 12 | using System.Management.Automation.Subsystem; 13 | using System.Management.Automation.Subsystem.Feedback; 14 | using System.Management.Automation.Subsystem.Prediction; 15 | 16 | namespace PSAdapterProvider 17 | { 18 | public sealed class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup 19 | { 20 | internal const string id = "6edf7436-db79-4b5b-b889-4e6d6a1c8680"; 21 | 22 | public void OnImport() 23 | { 24 | SubsystemManager.RegisterSubsystem(PSAdapterFeedbackPredictor.Singleton); 25 | SubsystemManager.RegisterSubsystem(PSAdapterFeedbackPredictor.Singleton); 26 | } 27 | 28 | public void OnRemove(PSModuleInfo psModuleInfo) 29 | { 30 | SubsystemManager.UnregisterSubsystem(new Guid(id)); 31 | SubsystemManager.UnregisterSubsystem(new Guid(id)); 32 | } 33 | } 34 | 35 | public sealed class PSAdapterFeedbackPredictor : IFeedbackProvider, ICommandPredictor 36 | { 37 | private readonly Guid _guid; 38 | private string? _suggestion; 39 | 40 | private SuggestionGenerator _suggestionGenerator; 41 | 42 | Dictionary? ISubsystem.FunctionsToDefine => null; 43 | 44 | /// 45 | /// add counter for cancellation token 46 | /// 47 | public static int FeedbackCancelCount { get; set; } 48 | 49 | /// 50 | /// add counter for cancellation token 51 | /// 52 | public static int SuggestionCancelCount { get; set; } 53 | public static int SuggestionRequestedCount { get; set; } 54 | 55 | /// 56 | /// Trigger for calling the predictor 57 | /// 58 | public FeedbackTrigger Trigger => FeedbackTrigger.All; 59 | 60 | private int suggestionAccepted = 0; 61 | private int suggestionDisplayed = 0; 62 | private int commandLineAccepted = 0; 63 | private int commandLineExecuted = 0; 64 | 65 | public static PSAdapterFeedbackPredictor Singleton { get; } = new PSAdapterFeedbackPredictor(Init.id); 66 | 67 | public PSAdapterFeedbackPredictor(string? guid = null) 68 | { 69 | if (guid is null) { 70 | _guid = Guid.NewGuid(); 71 | } else { 72 | _guid = new Guid(guid); 73 | } 74 | 75 | _suggestionGenerator = new SuggestionGenerator(); 76 | } 77 | 78 | public void Dispose() 79 | { 80 | _suggestionGenerator.Dispose(); 81 | } 82 | 83 | public Guid Id => _guid; 84 | 85 | public string Name => "PSAdapter"; 86 | 87 | public string Description => "Finds a JSON adapter for a native application."; 88 | 89 | /// 90 | /// Get feedback. 91 | /// 92 | public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token) 93 | { 94 | CommandAst? cAst = context.CommandLineAst.Find((ast) => ast is CommandAst, true) as CommandAst; 95 | if (cAst is not null) 96 | { 97 | /* 98 | // we need pipelines here because we need to check the potential next command in the pipeline 99 | // because if one of them is what we suggest, we don't want to suggest anything 100 | List? suggestedPipelines = _suggestionGenerator.GetSuggestedPipelines(cAst); 101 | if (suggestedPipelines is null || suggestedPipelines.Count == 0) 102 | { 103 | return null; 104 | } 105 | 106 | // Get the second command if it exists and compare it to the second command of the suggested pipelines 107 | PipelineAst? parent = cAst.Parent as PipelineAst; 108 | if (parent is not null && parent.PipelineElements.Count > 1) 109 | { 110 | string? secondCommand = null; 111 | secondCommand = (parent.PipelineElements[1] as CommandAst)?.GetCommandName(); 112 | if (secondCommand is not null) 113 | { 114 | foreach (PipelineAst suggestion in suggestedPipelines) 115 | { 116 | string? suggestionSecondCommand = (suggestion.PipelineElements[1] as CommandAst)?.GetCommandName(); 117 | if (suggestionSecondCommand is not null && suggestionSecondCommand.Equals(secondCommand)) 118 | { 119 | return null; 120 | } 121 | } 122 | } 123 | } 124 | 125 | List suggestions = new List(suggestedPipelines.Count); 126 | foreach(PipelineAst suggestion in suggestedPipelines) 127 | { 128 | suggestions.Add(suggestion.Extent.Text); 129 | } 130 | */ 131 | 132 | List? filteredSuggestions = GetFilteredSuggestions(cAst); 133 | if (filteredSuggestions is null) 134 | { 135 | return null; 136 | } 137 | 138 | return new FeedbackItem("PSAdapter found additional ways to run.", filteredSuggestions); 139 | } 140 | 141 | return null; 142 | } 143 | 144 | public List? GetFilteredSuggestions(CommandAst cAst) 145 | { 146 | // we need pipelines here because we need to check the potential next command in the pipeline 147 | // because if one of them is what we suggest, we don't want to suggest anything 148 | List? suggestedPipelines = _suggestionGenerator.GetSuggestedPipelines(cAst); 149 | if (suggestedPipelines is null || suggestedPipelines.Count == 0) 150 | { 151 | return null; 152 | } 153 | 154 | // Get the second command if it exists and compare it to the second command of the suggested pipelines 155 | PipelineAst? parent = cAst.Parent as PipelineAst; 156 | if (parent is not null && parent.PipelineElements.Count > 1) 157 | { 158 | string? secondCommand = null; 159 | secondCommand = (parent.PipelineElements[1] as CommandAst)?.GetCommandName(); 160 | if (secondCommand is not null) 161 | { 162 | foreach (PipelineAst suggestion in suggestedPipelines) 163 | { 164 | string? suggestionSecondCommand = (suggestion.PipelineElements[1] as CommandAst)?.GetCommandName(); 165 | if (suggestionSecondCommand is not null && suggestionSecondCommand.Equals(secondCommand)) 166 | { 167 | return null; 168 | } 169 | } 170 | } 171 | } 172 | 173 | List suggestions = new List(suggestedPipelines.Count); 174 | foreach(PipelineAst suggestion in suggestedPipelines) 175 | { 176 | suggestions.Add(suggestion.Extent.Text); 177 | } 178 | return suggestions; 179 | 180 | } 181 | 182 | private List? GetSuggestions(CommandAst commandAst) 183 | { 184 | List suggestionList = new List(1); 185 | string commandName = commandAst.GetCommandName(); 186 | 187 | List? suggestions = _suggestionGenerator.GetSuggestedPipelines(commandAst); 188 | if (suggestions is null) 189 | { 190 | return null; 191 | } 192 | 193 | foreach(PipelineAst suggestion in suggestions) 194 | { 195 | suggestionList.Add(new PredictiveSuggestion(suggestion.Extent.Text)); 196 | } 197 | 198 | return suggestionList; 199 | } 200 | 201 | public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback) 202 | { 203 | return feedback switch 204 | { 205 | PredictorFeedbackKind.CommandLineAccepted => true, 206 | _ => false, 207 | }; 208 | } 209 | 210 | public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken) 211 | { 212 | SuggestionRequestedCount++; 213 | CommandAst? commandAst = context.InputAst.Find((ast) => ast is CommandAst, true) as CommandAst; 214 | if(commandAst is null) 215 | { 216 | return default; 217 | } 218 | 219 | List? suggestions = _suggestionGenerator.GetSuggestedPipelines(commandAst); 220 | if (suggestions is null) 221 | { 222 | return default; 223 | } 224 | 225 | List result = new List(suggestions.Count); 226 | foreach(PipelineAst suggestion in suggestions) 227 | { 228 | result.Add(new PredictiveSuggestion(suggestion.Extent.Text)); 229 | } 230 | 231 | if (cancellationToken.IsCancellationRequested) 232 | { 233 | SuggestionCancelCount++; 234 | } 235 | 236 | return new SuggestionPackage(result); 237 | } 238 | 239 | public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) 240 | { 241 | commandLineAccepted++; 242 | } 243 | 244 | public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) { 245 | suggestionDisplayed++; 246 | } 247 | 248 | public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) { 249 | suggestionAccepted++; 250 | } 251 | 252 | public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { 253 | commandLineExecuted++; 254 | } 255 | 256 | } 257 | } 258 | --------------------------------------------------------------------------------