Creating GitHub Extension PowerShell Tools
In the last newsletter, I introduced you to extensions in the gh.exe
command-line tool for managing GitHub. I showed you some PowerShell code I had written to wrap around the native command-line tool and left other projects to you. But, as is typical, I couldn't leave well enough alone. Plus, I was bothered with a limitation in my previous code. Today, I want to show you how I wrapped the native gh.exe
commands for working with extensions in PowerShell.
A Better Search
One thing that bothered me about my previous code was the way emojis were handled. Some descriptions of extensions contain emojis,
gh extension search gh-f --sort updated

This also works with native JSON output.
gh extension search gh-f --sort updated --json fullName,url,updatedAt,description

But as soon as I convert the JSON to a PowerShell object, the emojis are lost.
gh extension search gh-f --sort updated --json fullName,url,updatedAt,description | ConvertFrom-Json | select Fullname,description

There is something in the gh.exe
output that prevents the emoji from being properly rendered.
The gh.exe
tool is using the native GitHub APIs, so started reverse-engineering the gh.exe
expression to return the same results using the GitHub API.
$search = "fzf"
$tr = Invoke-RestMethod -Uri "https://api.github.com/search/repositories?q=topic:gh-extension+$search+in:description" -Headers @{ "Accept" = "application/vnd.github.v3+json" }
$tr.items | ForEach-Object {
[PSCustomObject]@{
name = $_.name
description = $_.description
url = $_.html_url
}
}
The search is a compound query looking for the search term in the repository description and the topic gh-extension
. The results are JSON by default, so I can convert them to PowerShell objects

This is better in my opinion. With this proof-of-concept, I wrote this function.
Function Find-GHExtension {
[CmdletBinding()]
[OutputType('ghExtension')]
Param(
[Parameter(Position = 0, Mandatory, HelpMessage = 'What topic are you searching for?')]
[ValidateNotNullOrEmpty()]
[string]$SearchTopic
)
$splat = @{
headers = @{ 'Accept' = 'application/vnd.github.v3+json' }
uri = "https://api.github.com/search/repositories?q=topic:gh-extension+$SearchTopic+in:description"
}
$response = Invoke-RestMethod @splat
If ($response.total_count -gt 0) {
$response.items | ForEach-Object {
[PSCustomObject]@{
PSTypeName = 'ghExtension'
FullName = $_.full_name
Name = $_.name
Created = $_.created_at
Updated = $_.updated_at
Description = $_.description.trim()
Url = $_.html_url.trim()
Stars = $_.stargazers_count
Owner = $_.owner.login
}
}
}
else {
Write-Warning "No gh extensions found for $SearchTopic"
}
}
> ⚠ Warning > The GitHub API is subject to rate limits which you might hit if you run this repeatedly in a short time frame.
I included additional properties in the output object to make it more useful. I also added a type name to the object so I can create a custom format file to display the results in a more readable format. I will skip that for now.
Now, I get much better results.

Installing Extensions
Because I had a search function, it only made sense to have an install function. This function requires PowerShell 7,
Function Install-GHExtension {
[CmdletBinding(SupportsShouldProcess)]
[OutputType('none')]
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipelineByPropertyName,
ValueFromPipeline,
HelpMessage = 'What extension are you installing?'
)]
[ValidateNotNullOrEmpty()]
[ValidatePattern(('^\w+/\S+$'),ErrorMessage='The name should be in the "[HOST/]OWNER/REPO" format, got {0}')]
[string]$FullName
)
Process {
#Add support for WhatIf
If ($PSCmdlet.ShouldProcess($FullName)) {
$response = gh extension install $FullName
} #whatIf
} #process
}