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