Codesmithy.psm1



function Add-GitHubMetadata
{
<#
.SYNOPSIS
Adds GitHub Linguist overrides to a repo's .gitattributes.
 
.DESCRIPTION
There is a lot of metadata that should be added to a good repo.
This script simplifies adding much of that metadata.
 
.FUNCTIONALITY
Git and GitHub
 
.LINK
https://github.com/blog/2392-introducing-code-owners
 
.LINK
https://github.com/github/linguist#overrides
 
.LINK
https://github.com/blog/2111-issue-and-pull-request-templates
 
.LINK
https://help.github.com/articles/setting-guidelines-for-repository-contributors/
 
.LINK
https://help.github.com/articles/adding-a-license-to-a-repository/
 
.LINK
http://editorconfig.org/
 
.LINK
https://github.com/brianary/Detextive/
 
.LINK
Get-VSCodeSetting
 
.LINK
Set-VSCodeSetting
 
.LINK
Use-Command
 
.EXAMPLE
Add-GitHubMetadata -DefaultOwner arthurd@example.com -DefaultUsesTabs
 
Sets up the CODEOWNERS file and assigns a user, and sets the indent default.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','',
Justification='These plural nouns work with groups.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','',
Justification='Parameters are not tracked accurately.')]
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')][OutputType([void])] Param(
<#
Sets the code owner(s) by @username or email address to use when no more specific
code owners are provided. By default, any authors within a standard deviation of
the most commits will be included.
#>

[string[]] $DefaultOwner,
<#
Maps .gitattribute-style globbing syntax for file matching to @username or email
address of owners of any matching files.
#>

[hashtable] $Owners = @{},
<#
A list of .gitattribute-style globbing syntax matches for files that should be
considered vendor code, for files not covered by the default behavior of
the default Linguist vendor code file glob patterns:
https://github.com/github/linguist/blob/master/lib/linguist/vendor.yml
#>

[string[]] $VendorCode = @('**/packages/**','**/lib/**'),
<#
A list of .gitattribute-style globbing syntax matches for files that should be
considered documentation, for files not covered by the default behavior of
the default Linguist documentation file glob patterns:
https://github.com/github/linguist/blob/master/lib/linguist/documentation.yml
#>

[string[]] $DocumentationCode,
<#
A list of .gitattribute-style globbing syntax matches for files that should be
considered generated code, for files not covered by the default behavior of
the default Linguist generated code file glob patterns and contents matching:
https://github.com/github/linguist/blob/master/lib/linguist/generated.rb
#>

[string[]] $GeneratedCode = @('"**/Service References/**"','"**/Web References/**"'),
<#
A Markdown string containing a template for creating issues.
https://github.com/blog/2111-issue-and-pull-request-templates
#>

[string] $IssueTemplate,
<#
A Markdown string containing a template for creating pull requests.
https://github.com/blog/2111-issue-and-pull-request-templates
#>

[string] $PullRequestTemplate,
<#
The file path or URL containing guidelines for contributors in Markdown format.
https://help.github.com/articles/setting-guidelines-for-repository-contributors/
#>

[string] $ContributingFile,
<#
The file path or URL containing open source licensing for contributors in
Markdown format.
https://help.github.com/articles/adding-a-license-to-a-repository/
#>

[string] $LicenseFile,
<#
If no EditorConfig file exists, a simple default charset for text files in the
repo. By default this is set to the system default, which is often terrible.
#>

[string] $DefaultCharset = $OutputEncoding.WebName,
<#
If no EditorConfig file exists, a simple default line endings value for text files
in the repo. By default this is set to the system default, which is recommended.
#>

[string] $DefaultLineEndings = $(switch([Environment]::NewLine){"`n"{'lf'}"`r"{'cr'}default{'crlf'}}),
<#
If no EditorConfig file exists, a simple default number of characters to indent
lines for spaces (soft tabs) and tab display (hard tabs) for text files in the
repo.
#>

[int] $DefaultIndentSize = 4,
<#
If no EditorConfig file exists, this switch indicates a simple default for text
files in the repo to use tabs. Otherwise, spaces will be used for indentation.
#>

[switch] $DefaultUsesTabs,
<#
If no EditorConfig file exists, this switch indicates a simple default for text
files in the repo to preserve trailing spaces. Otherwise, trailing spaces will
be trimmed.
#>

[switch] $DefaultKeepTrailingSpace,
<#
If no EditorConfig file exists, this switch indicates a simple default for text
files in the repo not to add a final line ending at the end. Otherwise, a final
line ending will be added automatically if it is missing.
#>

[switch] $DefaultNoFinalNewLine,
# Indicates warnings about new content should be skipped.
[switch] $NoWarnings,
# Configure VSCode settings to recommend relevant extensions based on repo content.
[Alias('Recommendations')][switch] $VsCodeExtensionRecommendations,
<#
Configure VSCode settings to disable the Prettier extension for Markdown files,
since Prettier's formatting settings are not configurable, and some break Markdown
for some interpreters (e.g. lower-casing footnote links is incompatible with GitHub Pages),
and some formatting choices may not be desirable or may conflict with organizational
standards.
#>

[Alias('DisablePrettierMarkdown','NoPrettierMarkdown')][switch] $VSCodeDisablePrettierForMarkdown,
# Configure settings for Dev Containers, used for Codespaces.
[Alias('Codespaces')][switch] $DevContainer,
# Disables adding or updating the CODEOWNERS file.
[switch] $NoOwners,
# Do not prompt to append Linguist settings.
[switch] $Force
)

function Resolve-RepoPath
{
    [CmdletBinding()] Param([Parameter(ValueFromPipelineByPropertyName=$true)][Alias('FullName')][string] $Path)
    Process { (Resolve-Path $Path -Relative) -replace '^.\\','' -replace '\\','/' }
}

function Test-KeepFile
{
    [CmdletBinding(SupportsShouldProcess=$true)][OutputType([bool])] Param(
    [Parameter(Position=0)][string] $Filename,
    [switch] $Keep
    )
    $exists = Test-Path $Filename -Type Leaf
    if($exists -and $Keep) { Write-Verbose "Keeping existing $Filename"; return $true }
    if(!$exists) { return $false }
    Write-Verbose "$Filename exists"
    return !$PSCmdlet.ShouldProcess($Filename,'overwrite')
}

function Copy-GitHubFile
{
    [CmdletBinding()] Param(
    [Parameter(Position=0,Mandatory=$true)][string] $Filename,
    [Parameter(Position=1,Mandatory=$true)][Alias('Path','Url')][uri] $Source
    )
    if(Test-KeepFile $Filename){return}
    if($Source.IsFile){Copy-Item $Source.LocalPath $Filename}
    else{Invoke-WebRequest $Source -OutFile $Filename} #TODO: authentication for private repos?
}

function Add-GitHubDirectory
{
    [CmdletBinding()] Param()
    if(!(Test-Path .github -PathType Container)) {New-Item .github -Type Directory |Out-Null}
}

function Add-File
{
    [CmdletBinding()] Param(
    [Parameter(Position=0,Mandatory=$true)][string] $Filename,
    [Parameter(Position=1,Mandatory=$true)][string] $Contents,
    [Parameter(Position=2)][ValidateSet('utf8','ASCII')][string] $Encoding = 'utf8',
    [switch] $Warn,
    [switch] $Keep,
    [switch] $Force
    )
    if($Keep -and (Test-Path $Filename -Type Leaf)) { return }
    if(!$Contents){Write-Verbose "No contents to add to $Filename."; return }
    if(!$Force -and (Test-KeepFile $Filename)){ Write-Verbose "File $Filename exists!"; return }
    $Contents |Out-File $Filename -Encoding $Encoding
    git add -N $Filename |Out-Null
    if($Warn){ Write-Warning "The file $Filename has been added, be sure to review it and customize as needed." }
    Write-Verbose "Added $Filename"
}

function Add-Readme
{
    [CmdletBinding()] Param(
    [string] $name = (git rev-parse --show-toplevel |Split-Path -Leaf),
    [switch] $NoWarnings
    )
    if(Test-Path README.md -PathType Leaf){return}
    Add-File README.md @"
$name
$('='*$name.Length)
 
TODO: Summarize purpose of repo contents here.
 
Sections
--------
 
TODO: Add sections for additional details, special instructions, prerequisites, &c.
"@
 -Warn:$(!$NoWarnings)
}

function Add-CodeOwners
{
    [CmdletBinding()] Param(
    [string[]] $DefaultOwner,
    [hashtable] $Owners,
    [switch] $NoWarnings
    )
    if(Test-KeepFile .github/CODEOWNERS -Keep:(!$DefaultOwner -and !$Owners))
    {
        if('EFBBBF' -eq ('{0:X2}{1:X2}{2:X2}' -f (Get-Content .github/CODEOWNERS -AsByteStream -TotalCount 3)))
        {
            (Get-Content .github/CODEOWNERS -Raw).Trim() |Out-File .github/CODEOWNERS -Encoding utf8NoBOM
        }
        return
    }
    if(!$DefaultOwner)
    {
        Write-Verbose 'Determining default code owner(s).'
        $authors = git shortlog -nes HEAD |
            Select-String '^\s*(?<Commits>\d+)\s+(?<Name>\b[^>]+\b)\s+<(?<Email>[^>]+)>$' |
            ForEach-Object {[pscustomobject]@{
                Commits = $_.Matches.Groups[1].Value
                Name    = $_.Matches.Groups[2].Value
                Email   = $_.Matches.Groups[3].Value
            }}
        $authors |Out-String |Write-Verbose
        [int] $max = ($authors |Measure-Object Commits -Maximum).Maximum
        [int] $oneSigmaFromTop = $max - ($authors.Commits |Measure-Object -StandardDeviation).StandardDeviation
        Write-Verbose "Authors with $oneSigmaFromTop or more commits will be included as default code owners."
        $DefaultOwner = $authors |Where-Object {[int] $_.Commits -ge $oneSigmaFromTop} |Select-Object -ExpandProperty Email
        Write-Verbose "Default code owners determined to be $DefaultOwner."
    }
    $Local:OFS = [Environment]::NewLine
    Add-File -Filename .github/CODEOWNERS -Contents @"
 
# Code Owners file https://github.com/blog/2392-introducing-code-owners
# .gitattributes selection syntax mapping to GitHub @usernames or email addresses.
 
# default owner(s)
* $DefaultOwner
$(if($Owners){"$OFS# targeted owners"})
$($Owners.Keys |ForEach-Object {"$_ $($Owners[$_] -join ' ')"})
"@
 -Encoding ASCII -Warn:$(!$NoWarnings) -Force
}

function Add-LinguistOverrides
{
    [CmdletBinding(SupportsShouldProcess=$true)] Param(
    [string[]] $VendorCode,
    [string[]] $DocumentationCode,
    [string[]] $GeneratedCode,
    [switch] $Force
    )
    if(!(Test-Path .gitattributes -PathType Leaf))
    {
        Write-Verbose 'Creating .gitattributes file.'
        '','# Linguist overrides https://github.com/github/linguist#overrides' |Out-File .gitattributes ascii
    }
    else
    {
        if('EFBBBF' -eq ('{0:X2}{1:X2}{2:X2}' -f (Get-Content .gitattributes -AsByteStream -TotalCount 3)))
        {
            (Get-Content .github/CODEOWNERS -Raw).Trim() |Out-File .gitattributes -Encoding utf8NoBOM
        }
        if(Select-String '^# Linguist overrides' .gitattributes)
        {
            Select-String '^# Linguist overrides|\blinguist-\w+' .gitattributes |Out-String |Write-Verbose
            if(!$Force -and !$PSCmdlet.ShouldContinue('.gitattributes','append Linguist overrides'))
            {
                Write-Verbose 'The .gitattributes file already contains a "Linguist overrides" section.'
                return
            }
        }
        else
        {
            '','# Linguist overrides https://github.com/github/linguist#overrides' |Add-Content .gitattributes -Encoding UTF8
        }
    }
    if($VendorCode) {$VendorCode |ForEach-Object {"$_ linguist-vendored"} |Add-Content .gitattributes -Encoding UTF8}
    if($DocumentationCode) {$DocumentationCode |ForEach-Object {"$_ linguist-documentation"} |Add-Content .gitattributes -Encoding UTF8}
    if($GeneratedCode) {$GeneratedCode |ForEach-Object {"$_ linguist-generated=true"} |Add-Content .gitattributes -Encoding UTF8}
    #TODO: linguist-language entries?
    git add -N .gitattributes |Out-Null
    Write-Verbose 'Added Linguist overrides section to .gitattributes.'
    Select-String '^# Linguist overrides|\blinguist-\w+' .gitattributes |Out-String |Write-Verbose
}

function Add-IssueTemplate
{
    [CmdletBinding()] Param(
    [string] $IssueTemplate
    )
    if(!$IssueTemplate){Write-Verbose 'No issue template.'; return}
    Add-File .github/ISSUE_TEMPLATE.md $IssueTemplate
}

function Add-PullRequestTemplate
{
    [CmdletBinding()] Param(
    [string] $PullRequestTemplate
    )
    if(!$PullRequestTemplate){Write-Verbose 'No pull request template.'; return}
    Add-File .github/PULL_REQUEST_TEMPLATE.md $PullRequestTemplate
}

function Add-ContributingGuidelines
{
    [CmdletBinding()] Param(
    [string] $ContributingFile
    )
    if(!$ContributingFile){Write-Verbose 'No contributing file.'; return}
    Copy-GitHubFile .github/CONTRIBUTING.md $ContributingFile
}

function Add-License
{
    [CmdletBinding()] Param(
    [string] $LicenseFile
    )
    if(!$LicenseFile){Write-Verbose 'No license.'; return}
    Copy-GitHubFile LICENSE.md $LicenseFile
}

function Add-EditorConfig
{
    [CmdletBinding()] Param(
    [string] $DefaultCharset,
    [string] $DefaultLineEndings,
    [int] $DefaultIndentSize,
    [switch] $DefaultUsesTabs,
    [switch] $DefaultKeepTrailingSpace,
    [switch] $DefaultNoFinalNewLine,
    [switch] $NoWarnings
    )
    Add-File .editorconfig @"
# EditorConfig is awesome: http://EditorConfig.org
 
# last word for the project
root = true
 
# defaults
[*]
indent_style = $(if($DefaultUsesTabs){'tab'}else{'space'})
indent_size = $DefaultIndentSize
tab_width = $DefaultIndentSize
end_of_line = $DefaultLineEndings
charset = $DefaultCharset
trim_trailing_whitespace = $(if($DefaultKeepTrailingSpace){'false'}else{'true'})
insert_final_newline = $(if($DefaultNoFinalNewLine){'false'}else{'true'})
 
# git
[{.gitattributes,CODEOWNERS}]
charset = utf-8
 
# CSS
# https://www.w3.org/International/questions/qa-utf8-bom.en#bytheway
[*.css]
charset = utf-8
 
"@
 -Warn:$(!$NoWarnings) -Keep
}

function Add-VsCodeExtensionRecommendations
{
    [CmdletBinding()] Param()
    if(!(Test-Path .vscode -PathType Container)) {New-Item .vscode -Type Directory |Out-Null}
    $recommendations = New-Object Collections.Generic.HashSet[string]
    [string[]] $previous = Get-VSCodeSetting /recommendations -Workspace
    if($previous) {$previous |ForEach-Object {[void]$recommendations.Add($_)}}
    if((Test-Path .github -Type Container) -and (Test-Path .github/workflows -Type Container) -and
        (Get-ChildItem .github/workflows -Filter *.yml |Select-Object -First 1))
    {
        [void]$recommendations.Add('GitHub.vscode-github-actions')
    }
    [void]$recommendations.Add('yzhang.markdown-all-in-one')
    [void]$recommendations.Add('EditorConfig.EditorConfig')
    if(Get-ChildItem -Recurse -Filter *.md |Select-String '```mermaid' |Select-Object -First 1)
    {
        [void]$recommendations.Add('bierner.markdown-mermaid')
        [void]$recommendations.Add('bpruitt-goddard.mermaid-markdown-syntax-highlighting')
    }
    if(Get-ChildItem -Recurse -Filter *.adoc |Select-Object -First 1)
    {
        [void]$recommendations.Add('asciidoctor.asciidoctor-vscode')
    }
    if(Get-ChildItem -Recurse -Filter *.http |Select-Object -First 1)
    {
        [void]$recommendations.Add('humao.rest-client')
    }
    if(Get-ChildItem -Recurse -Filter *.ps1 |Select-Object -First 1)
    {
        [void]$recommendations.Add('ms-vscode.powershell')
    }
    if(Get-ChildItem -Recurse -Filter *.sql |Select-Object -First 1)
    {
        [void]$recommendations.Add('ms-mssql.mssql')
    }
    Set-VSCodeSetting /recommendations $recommendations -Workspace
}

function Add-DevContainerSettings
{
    [CmdletBinding()] Param()
    if(Test-Path .devcontainer/devcontainer.json -Type Leaf)
    {
        ${devcontainer.json} = '.devcontainer/devcontainer.json'
        $settings = Get-Content ${devcontainer.json} |ConvertFrom-Json
    }
    elseif(Test-Path .devcontainer.json -Type Leaf)
    {
        ${devcontainer.json} = '.devcontainer.json'
        $settings = Get-Content ${devcontainer.json} |ConvertFrom-Json
    }
    else
    {
        ${devcontainer.json} = '.devcontainer.json'
        $settings = [pscustomobject]@{
            customizations = [pscustomobject]@{
                vscode = [pscustomobject]@{
                    settings=[pscustomobject]@{}
                    extensions=@()
                }
            }
        }
    }
    if(!$settings.PSObject.Properties.Match('customizations').Count)
    {
        $settings |Add-Member -NotePropertyName customizations -NotePropertyValue ([pscustomobject]@{
            vscode = @{
                settings=[pscustomobject]@{}
                extensions=@()
            }
        })
    }
    elseif(!$settings.customizations.PSObject.Properties.Match('vscode').Count)
    {
        $settings.customizations |Add-Member -NotePropertyName vscode -NotePropertyValue ([pscustomobject]@{
            settings=[pscustomobject]@{}
            extensions=@()
        })
    }
    elseif(!$settings.customizations.vscode.PSObject.Properties.Match('extensions').Count)
    {
        $settings.customizations.vscode |Add-Member -NotePropertyName extensions -NotePropertyValue @()
    }
    $extensions = $settings.customizations.vscode.extensions -isnot [array] ?
        $settings.customizations.vscode.extensions : @()
    if(Get-ChildItem .github/workflows -Filter *.yml |Select-Object -First 1)
    {
        $extensions += 'GitHub.vscode-github-actions'
    }
    if(Get-ChildItem -Recurse -Filter *.md |Select-Object -First 1)
    {
        $extensions += 'DavidAnson.vscode-markdownlint'
    }
    if(Get-ChildItem -Recurse -Filter *.md |Select-String '```mermaid' |Select-Object -First 1)
    {
        $extensions += 'bierner.markdown-mermaid'
        $extensions += 'bpruitt-goddard.mermaid-markdown-syntax-highlighting'
    }
    if(Get-ChildItem -Recurse -Filter *.adoc |Select-Object -First 1)
    {
        $extensions += 'asciidoctor.asciidoctor-vscode'
    }
    $settings.customizations.vscode.extensions = @($extensions |Select-Object -Unique)
    $settings |ConvertTo-Json -Depth 5 |Out-File ${devcontainer.json} utf8
}

function Disable-VsCodePrettier
{
    [CmdletBinding()] Param()
    if(!(Test-Path .vscode -PathType Container)) {New-Item .vscode -Type Directory |Out-Null}
    Set-VSCodeSetting /prettier.disableLanguages @('markdown') -Workspace
    Set-VSCodeSetting '/[markdown]' @{
        'editor.defaultFormatter' = 'yzhang.markdown-all-in-one'
    } -Workspace
}

function Add-Metadata
{
    [CmdletBinding()] Param(
    [string[]] $DefaultOwner,
    [hashtable] $Owners,
    [string[]] $VendorCode,
    [string[]] $DocumentationCode,
    [string[]] $GeneratedCode,
    [string] $IssueTemplate,
    [string] $PullRequestTemplate,
    [string] $ContributingFile,
    [string] $LicenseFile,
    [string] $DefaultCharset,
    [string] $DefaultLineEndings,
    [int] $DefaultIndentSize,
    [switch] $DefaultUsesTabs,
    [switch] $DefaultKeepTrailingSpace,
    [switch] $DefaultNoFinalNewLine,
    [switch] $NoWarnings,
    [switch] $Force
    )
    if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
    Push-Location $(git rev-parse --show-toplevel)
    Add-GitHubDirectory
    Add-Readme -NoWarnings:$NoWarnings
    if(!$NoOwners) {Add-CodeOwners -DefaultOwner $DefaultOwner -Owners $Owners -NoWarnings:$NoWarnings}
    Add-LinguistOverrides -VendorCode $VendorCode -DocumentationCode $DocumentationCode `
        -GeneratedCode $GeneratedCode -Force:$Force
    Add-IssueTemplate -IssueTemplate $IssueTemplate
    Add-PullRequestTemplate -PullRequestTemplate $PullRequestTemplate
    Add-ContributingGuidelines -ContributingFile $ContributingFile
    Add-License -LicenseFile $LicenseFile
    Add-EditorConfig -DefaultCharset $DefaultCharset -DefaultLineEndings $DefaultLineEndings `
        -DefaultIndentSize $DefaultIndentSize -DefaultUsesTabs:$DefaultUsesTabs `
        -DefaultKeepTrailingSpace:$DefaultKeepTrailingSpace -DefaultNoFinalNewLine:$DefaultNoFinalNewLine `
        -NoWarnings:$NoWarnings
    if($VsCodeExtensionRecommendations) {Add-VsCodeExtensionRecommendations}
    if($VSCodeDisablePrettierForMarkdown) {Disable-VsCodePrettier}
    if($DevContainer) {Add-DevContainerSettings}
    Pop-Location
}

Add-Metadata -DefaultOwner $DefaultOwner -Owners $Owners -VendorCode $VendorCode `
    -DocumentationCode $DocumentationCode -GeneratedCode $GeneratedCode -IssueTemplate $IssueTemplate `
    -PullRequestTemplate $PullRequestTemplate -ContributingFile $ContributingFile -LicenseFile $LicenseFile `
    -DefaultCharset $DefaultCharset -DefaultLineEndings $DefaultLineEndings -DefaultIndentSize $DefaultIndentSize `
    -DefaultUsesTabs:$DefaultUsesTabs -DefaultKeepTrailingSpace:$DefaultKeepTrailingSpace `
    -DefaultNoFinalNewLine:$DefaultNoFinalNewLine -NoWarnings:$NoWarnings -Force:$Force

}

function Add-NotebookCell
{
<#
.SYNOPSIS
When run within a Polyglot Notebook, appends a cell to it.
 
.PARAMETER Language
The cell language to use.
 
.FUNCTIONALITY
Notebooks
 
.INPUTS
Any object that can be converted to a string and used as cell content.
 
.EXAMPLE
"flowchart LR`nA --> B" |Add-NotebookCell mermaid
 
Appends a cell with a Mermaid chart.
#>


[CmdletBinding()] Param(
# The cell content.
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][psobject] $InputObject
)
DynamicParam
{
    try {[Kernel]::Root.ChildKernels.Name |Add-DynamicParam.ps1 Language string}
    catch {'csharp','fsharp','html','http','javascript','kql','mermaid','pwsh','sql','value','vscode' |Add-DynamicParam.ps1 Language string}
    $DynamicParams
}
End
{
    if(!$PSBoundParameters.ContainsKey('Language'))
    {
        [Kernel]::Root.SendAsync((New-Object SendEditableCode 'vscode',($input |Out-String))) |Out-Null
    }
    else
    {
        [Kernel]::Root.SendAsync((New-Object SendEditableCode $PSBoundParameters['Language'],($input |Out-String))) |Out-Null
    }
}

}

function Add-VsCodeDatabaseConnection
{
<#
.SYNOPSIS
Adds a VS Code MSSQL database connection to the repo.
 
.DESCRIPTION
The VSCode MSSQL extension can use saved database connections to connect to for queries,
and this allows adding those to the VSCode settings in the current git repo.
 
.INPUTS
Any object with these properties, used to construct a database connection entry:
* ProfileName or Name
* ServerInstance or Server or DataSource
* Database or InitialCatalog
* UserName or UID
 
.FUNCTIONALITY
VSCode
 
.LINK
https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql
 
.LINK
Get-VSCodeSetting
 
.LINK
Set-VSCodeSetting
 
.EXAMPLE
Add-VsCodeDatabaseConnection ConnectionName ServerName\instance DatabaseName
 
Adds an MSSQL extension trusted connection named ConnectionName that
connects to the server ServerName\instance and database DatabaseName.
#>


[CmdletBinding()][OutputType([void])] Param(
# The name of the connection.
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[Alias('Name')][string] $ProfileName,
# The name of a server (and optional instance) to connect and use for the query.
[Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[Alias('Server','DataSource')][string] $ServerInstance,
# The the database to connect to on the server.
[Parameter(Position=2,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[Alias('InitialCatalog')][string] $Database,
<#
The username to connect with. No password will be stored.
If no username is given, a trusted connection will be created.
#>

[Parameter(Position=3,ValueFromPipelineByPropertyName=$true)]
[Alias('UID')][string] $UserName,
# Overwrite an existing profile with the same name.
[switch] $Force
)
Begin
{
    [psobject[]]$connections = Get-VSCodeSetting /mssql.connections -Workspace
    if(!$connections) {[psobject[]]$connections = @()}
}
Process
{
    if($connections |Where-Object profileName -eq $ProfileName)
    {
        if($Force) {$connections = $connections |Where-Object profileName -ne $ProfileName}
        else {Write-Verbose "Connection '$ProfileName' already exists"; return}
    }
    $connections +=
        if($UserName)
        {[pscustomobject]@{
            server             = $ServerInstance
            database           = $Database
            authenticationType = 'SqlLogin'
            profileName        = $ProfileName
            password           = ''
            user               = $UserName
            savePassword       = $false
        }}
        else
        {[pscustomobject]@{
            server             = $ServerInstance
            database           = $Database
            authenticationType = 'Integrated'
            profileName        = $ProfileName
            password           = ''
        }}
}
End
{
    $connections |ConvertTo-Json -Compress |Write-Verbose
    Set-VSCodeSetting /mssql.connections $connections -Workspace
}

}

function Copy-GitHubLabels
{
<#
.SYNOPSIS
Copies configured issue labels from one repo to another.
 
.FUNCTIONALITY
Git and GitHub
 
.INPUTS
An object with these properties:
* owner or DestinationOwnerName (optional)
* name or DestinationRepositoryName
 
.LINK
Get-GitHubLabel
 
.LINK
New-GitHubLabel
 
.LINK
Set-GitHubLabel
 
.LINK
Remove-GitHubLabel
 
.EXAMPLE
Copy-GitHubLabels -OwnerName brianary -RepositoryName scripts -DestinationRepositoryName webcoder
 
Inserts new labels from the brianary/scripts repo to the brianary/webcoder repo, and also
updates attributes like description and color from matching labels in the source.
#>


[CmdletBinding()] Param(
# The source repository's owner name.
[Parameter(Position=0,Mandatory=$true)][string] $OwnerName,
# The source repository name.
[Parameter(Position=1,Mandatory=$true)][string] $RepositoryName,
# The destination repository's owner name.
[Parameter(ValueFromPipelineByPropertyName=$true)][Alias('owner')][string] $DestinationOwnerName = $OwnerName,
# The destination repository name.
[Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Alias('name')][string] $DestinationRepositoryName,
<#
Determines the copy behavior:
* AddNew: Insert new labels from the source.
* AddAndUpdate: Insert new labels from the source, and also overwrite attributes from matching labels in the source.
* ReplaceAll: Insert new labels from the source, overwrite attributes from matching labels in the source, and delete
  any labels that don't exist in the source.
#>

[ValidateSet('AddAndUpdate','AddNew','ReplaceAll')][string] $Mode = 'AddAndUpdate'
)
Begin
{
    [pscustomobject[]] $source = Get-GitHubLabel -OwnerName $OwnerName -RepositoryName $RepositoryName
}
Process
{
    $destination = @{ OwnerName = $DestinationOwnerName; RepositoryName = $DestinationRepositoryName }
    [pscustomobject[]] $labels = Get-GitHubLabel @destination
    $source |Where-Object LabelName -NotIn $labels.LabelName |
        ForEach-Object {New-GitHubLabel @destination -Label $_.name -Color $_.color -Description $_.description}
    if($Mode -ne 'AddNew')
    {
        $source |Where-Object LabelName -In $labels.LabelName |
            ForEach-Object {Set-GitHubLabel @destination -Label $_.name -Color $_.color -Description $_.description}
        if($Mode -eq 'ReplaceAll')
        {
            $labels |Where-Object LabelName -NotIn $source.LabelName |Remove-GitHubLabel @destination
        }
    }
}

}

function Export-OpenApiSchema
{
<#
.SYNOPSIS
Extracts a JSON schema from an OpenAPI definition.
 
.OUTPUTS
System.String of the extracted JSON schema.
 
.FUNCTIONALITY
Json
 
.LINK
https://www.openapis.org/
 
.LINK
Export-Json
 
.LINK
Set-Json
 
.EXAMPLE
Export-OpenApiSchema api.json
 
Returns the schema of the 200 response of any defined endpoint is returned.
 
.EXAMPLE
Export-OpenApiSchema api.json POST /hello -RequestSchema
 
Returns the schema of the request body of the POST /hello endpoint.
#>


[CmdletBinding(DefaultParameterSetName='ResponseStatus')][OutputType([string])] Param(
# The path to the OpenAPI JSON file.
[Parameter(Position=0)][string] $Path,
# The HTTP verb of the endpoint to extract the schema from.
[Parameter(Position=1)][string] $Method = '*',
# The HTTP path of the endpoint to extract the schema from.
[Parameter(Position=2)][Alias('ApiPath')][string] $EndpointPath = '*',
# Indicates that the request schema of the endpoint should be returned.
[Parameter(ParameterSetName='RequestSchema')][Alias('In')][switch] $RequestSchema,
# Indicates the HTTP status code of the response schema of the endpoint that should be returned.
[Parameter(ParameterSetName='ResponseStatus',Position=3)][int] $ResponseStatus = 200
)
Process
{
    $Method = $Method.ToLowerInvariant()
    return ($PSCmdlet.ParameterSetName -eq 'RequestSchema' `
        ? (Export-Json "/paths/$EndpointPath/$Method/parameters/*/schema" -Path $Path)
        : (Export-Json "/paths/$EndpointPath/$Method/responses/$ResponseStatus/content/*/schema" -Path $Path) ) |
        Set-Json '/$schema' 'http://json-schema.org/draft-04/schema#'
}

}

function Find-DotNetTools
{
<#
.SYNOPSIS
Returns a list of matching dotnet tools.
 
.FUNCTIONALITY
DotNet
 
.EXAMPLE
Find-DotNetTools interactive |Format-Table -AutoSize
 
PackageName Version Authors Downloads Verified
----------- ------- ------- --------- --------
microsoft.dotnet-interactive 1.0.516401 Microsoft 33682741 True
dotnet-repl 0.1.216 jonsequitur 117599 False
#>


[CmdletBinding()] Param(
# The name of the tool to search for.
[Parameter(Position=0,Mandatory=$true)][string] $Name
)

if(!(Get-Command dotnet -Type Application -ErrorAction Ignore))
{
    throw 'The dotnet CLI was not found.'
}
foreach($line in dotnet tool search $Name |Where-Object {$_ -match '^\S+\s+\d+(?:\.\d+)+\b'})
{
    $package,$version,$authors,$downloads,$verified = $line -split '\s\s+',5
    [pscustomobject]@{
        PackageName = $package
        Version     = try{[semver]$version}catch{try{[version]$version}catch{$version}};
        Authors     = $authors
        Downloads   = [long]$downloads
        Verified    = $verified.Trim() -eq 'x'
    }
}

}

function Get-AssemblyFramework
{
<#
.SYNOPSIS
Gets the framework version an assembly was compiled for.
 
.INPUTS
Objects with System.String properties named Path or FullName.
 
.OUTPUTS
System.Management.Automation.PSCustomObject with RuntimeVersion and CompileVersion properties.
 
.FUNCTIONALITY
DotNet
 
.LINK
https://stackoverflow.com/questions/3460982/determine-net-framework-version-for-dll#25649840
 
.EXAMPLE
Get-AssemblyFramework Program.exe
 
RuntimeVersion CompileVersion
-------------- --------------
v4.0.30319 .NETFramework,Version=v4.7.2
#>


[CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param(
# The assembly to get the framework version of.
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Alias('FullName')][string] $Path
)
Process
{
    $assembly = [Reflection.Assembly]::ReflectionOnlyLoadFrom((Resolve-Path $Path))
    [PSCustomObject]@{
        RuntimeVersion = $assembly.ImageRuntimeVersion
        CompileVersion = $assembly.CustomAttributes |
            Where-Object {$_.AttributeType.Name -eq "TargetFrameworkAttribute" } |
            ForEach-Object {$_.ConstructorArguments.value}
    }
}

}

function Get-GitFileMetadata
{
<#
.SYNOPSIS
Returns the creation and last modification metadata for a file in a git repo.
 
.FUNCTIONALITY
Git and GitHub
 
.LINK
Get-ChildItem
 
.LINK
Resolve-Path
 
.EXAMPLE
Get-GitFileMetadata README.md
 
Path : .\README.md
CreateCommit : 1fde7af
CreateAuthor : Brian Lalonde
CreateEmail : brianary@example.com
CreateDate : 01/19/2015 11:44:15
LastCommit : dbe27ba
LastAuthor : Brian Lalonde
LastEmail : brianary@example.com
LastDate : 12/07/2020 20:17:15
#>


[CmdletBinding()][OutputType([psobject])] Param(
# The path (or paths) to get metadata for.
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$true)]
[string[]] $Path,
# Recurse into subdirectories.
[switch] $Recurse
)
Begin
{
    if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
}
Process
{
    foreach($f in Get-ChildItem $Path -Recurse:$Recurse)
    {
        $create_commit,$create_author,$create_email,$create_date =
            (git log --reverse --format="%h%x09%cn%x09%ae%x09%ai" $f |Select-Object -f 1) -split '\t'
        $last_commit,$last_author,$last_email,$last_date =
            (git log -1 --format="%h%x09%cn%x09%ae%x09%ai" $f |Select-Object -f 1) -split '\t'
        [pscustomobject]@{
            Path = Resolve-Path $f -Relative
            CreateCommit = $create_commit
            CreateAuthor = $create_author
            CreateEmail = $create_email
            CreateDate = [datetime]$create_date
            LastCommit = $last_commit
            LastAuthor = $last_author
            LastEmail = $last_email
            LastDate = [datetime]$last_date
        }
    }
}

}

function Get-GitFirstCommit
{
<#
.SYNOPSIS
Gets the SHA-1 hash of the first commit of the current repo.
 
.OUTPUTS
System.String containing the SHA-1 hash of this repo's first commit.
 
.FUNCTIONALITY
Git and GitHub
 
.EXAMPLE
Get-GitFirstCommit
 
1fde7af20e8560c720d42227495e8d15459aafa4
#>


[CmdletBinding()][OutputType([string])] Param()
if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
git log --max-parents=0 --format=format:%H HEAD

}

function Get-GitHubRepoChildItem
{
<#
.SYNOPSIS
Gets the items and child items in one or more specified locations.
 
.EXAMPLE
Get-GitHubRepoChildItem -Filter *.csproj -Recurse -File -OwnerName PowerShell -RepositoryName PSScriptAnalyzer |Format-Table name,size,path -AutoSize
 
name size path
---- ---- ----
Engine.csproj 3679 Engine/Engine.csproj
Rules.csproj 2586 Rules/Rules.csproj
 
.EXAMPLE
Get-GitHubRepoChildItem -Path src -AlternatePath / -Filter LICENSE -File -OwnerName PowerShell -RepositoryName PSScriptAnalyzer
 
name : LICENSE
path : LICENSE
sha : cec380d8ef7f7a1ad3ff9ef2356a88a13a78b491
size : 1089
url : https://api.github.com/repos/PowerShell/PSScriptAnalyzer/contents/LICENSE?ref=master
html_url : https://github.com/PowerShell/PSScriptAnalyzer/blob/master/LICENSE
git_url : https://api.github.com/repos/PowerShell/PSScriptAnalyzer/git/blobs/cec380d8ef7f7a1ad3ff9ef2356a88a13a78b491
download_url : https://raw.githubusercontent.com/PowerShell/PSScriptAnalyzer/master/LICENSE
type : file
_links : @{self=https://api.github.com/repos/PowerShell/PSScriptAnalyzer/contents/LICENSE?ref=master; git=https://api.github.com/repos/PowerShell/PSScriptAnalyzer/git/blobs/cec380d8ef7f7a1ad3ff9ef2356a88a13a78b491; html=https://github.com/PowerShell/PSScriptAnalyzer/blob/master/LICENSE}
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars','',
Justification='Using a global variable to cache responses, to avoid abusive API iteration.')]
[CmdletBinding()] Param(
# The path for which to retrieve contents.
[Parameter(Position=0)][string] $Path = '',
# Specifies a wildcard pattern to filter matches against.
[string] $Filter = '*',
# Specifies a wildcard pattern to exclude matches against.
[string] $Exclude = '',
# An alternate path to retrieve if the primary Path isn't found.
[string] $AlternatePath,
# Indicates subdirectories should be searched.
[switch] $Recurse,
# Indicates that only files should be returned.
[switch] $File,
# Indicates that only directories should be returned.
[switch] $Directory,
# Owner of the repository.
[Parameter(ValueFromPipelineByPropertyName)][psobject] $OwnerName,
# Name of the repository.
[Parameter(ValueFromPipelineByPropertyName)][Alias('Name')][string] $RepositoryName,
# The branch, or defaults to the default branch of not specified.
[string] $BranchName,
# Ignores any cached result and re-queries the GitHub API.
[switch] $Force
)
Begin
{
    if(!(Get-Variable GitHubRepoContents -Scope Global -ErrorAction SilentlyContinue)) {$Global:GitHubRepoContents = @{}}
    function Get-PathContentOrAlternate
    {
        [CmdletBinding()] Param(
        [psobject] $OwnerName,
        [string] $RepositoryName,
        [string] $Path,
        [string] $Branch,
        [string] $AlternatePath
        )
        [void]$PSBoundParameters.Remove('AlternatePath')
        if($Path -in '','.','/') {[void]$PSBoundParameters.Remove('Path')}
        try {return Get-GitHubContent @PSBoundParameters}
        catch [Microsoft.PowerShell.Commands.HttpResponseException]
        {
            Write-Verbose "Could not find path $Path in $OwnerName/$RepositoryName"
            if($_.Exception.Response.StatusCode -ne 404 -or $AlternatePath -eq $null) {throw}
            if($AlternatePath -in '','.','/') {[void]$PSBoundParameters.Remove('Path')}
            else {$PSBoundParameters['Path'] = $AlternatePath}
            return Get-GitHubContent @PSBoundParameters
        }
    }
}
Process
{
    Write-Verbose $MyInvocation.Line
    if($OwnerName -isnot [string]) {$PSBoundParameters['OwnerName'] = $OwnerName = $OwnerName.UserName}
    $repoContext, $searchContext = "$OwnerName/$RepositoryName/$BranchName",
        "$Path|$AlternatePath|$Filter|$Exclude|$Recurse|$File|$Directory"
    if(!$Global:GitHubRepoContents.ContainsKey($repoContext))
    {
        $Global:GitHubRepoContents[$repoContext] = @{}
    }
    elseif(!$Force -and $Global:GitHubRepoContents[$repoContext].ContainsKey($searchContext))
    {
        return $Global:GitHubRepoContents[$repoContext][$searchContext]
    }
    $entryType = if($File -and $Directory) {''} elseif($File) {'file'} elseif($Directory) {'dir'} else {'*'}
    $contentSpec = @{
        OwnerName = $OwnerName
        RepositoryName = $RepositoryName
        Path = $Path
        AlternatePath = $AlternatePath
    }
    if($BranchName) {$contentSpec += @{BranchName=$BranchName}}
    Write-Progress "Searching $OwnerName/$RepositoryName $BranchName" ( $Path ? $Path : '/' )
    if(!$Force -and $Global:GitHubRepoContents[$repoContext].ContainsKey("$Path|$AlternatePath"))
    {
        $content = $Global:GitHubRepoContents[$repoContext]["$Path|$AlternatePath"]
    }
    else
    {
        $content = Get-PathContentOrAlternate @contentSpec
        $Global:GitHubRepoContents[$repoContext]["$Path|$AlternatePath"] = $content
    }
    if($content.type -eq 'file')
    {
        if($content.type -notlike $entryType -or $content.name -notlike $Filter -or $content.name -like $Exclude) {return}
        $Global:GitHubRepoContents[$repoContext][$searchContext] = $content
        return $content
    }
    $found = @($content.entries |
        Where-Object {$_.type -like $entryType -and $_.name -like $Filter -and $_.name -notlike $Exclude})
    if($Recurse)
    {
        $found += @($content.entries |
            Where-Object {$_.type -eq 'dir' -and $_.name -notlike $Exclude} |
            ForEach-Object {& $PSCommandPath -Path $_.path -Filter $Filter -Exclude $Exclude -Recurse -File:$File `
                -Directory:$Directory -OwnerName $OwnerName -RepositoryName $RepositoryName -BranchName $BranchName})
    }
    $Global:GitHubRepoContents[$repoContext][$searchContext] = $found
    Write-Progress "Searching $OwnerName/$RepositoryName $BranchName" -Completed
    return $found
}

}

function Get-GitRepoChangeStatus
{
<#
.SYNOPSIS
Indicates whether there are outgoing changes in the local repo or incoming ones from the remote repo.
 
.INPUTS
System.IO.DirectoryInfo
 
.LINK
https://git-scm.com/docs
 
.EXAMPLE
Get-ChildItem ~/GitHub -Directory |Get-GitRepoChangeStatus |Format-Table
 
In Out Repository
-- --- ----------
   ↑ Codesmithy
   ↑ Databaseline
       SelectHtml
       SelectXmlExtensions
   ↑ Unicodery
↓ ↑ webcoder
#>


[CmdletBinding()] Param(
# The directory containing the local git repository.
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][IO.DirectoryInfo] $Repository,
# Indicates that Emoji status characters should be returned.
[switch] $AsEmoji
)
Begin
{
    $Script:incoming,$Script:outgoing = $AsEmoji ? @('📥️','📤️') : @('↓','↑')
}
Process
{
    try
    {
        Push-Location $Repository.FullName
        return [pscustomobject]@{
            In         = try{(git diff --summary '@{u}') ? $incoming : $null} catch {$_};
            Out        = try{(git diff --name-only '@{u}') ? $outgoing : $null} catch {$_};
            Repository = $Repository.Name
        }
    }
    finally {Pop-Location}
}

}

function Get-LibraryVulnerabilityInfo
{
<#
.SYNOPSIS
Get the list of module/package/library vulnerabilities from the RetireJS or SafeNuGet projects.
 
.INPUTS
System.String of a package/module name to search for.
 
.OUTPUTS
System.Management.Automation.PSCustomObject with details about any vulnerabilities found.
 
.FUNCTIONALITY
Packages and libraries
 
.LINK
Invoke-RestMethod
 
.LINK
Select-Xml
 
.EXAMPLE
Get-LibraryVulnerabilityInfo Backbone.js
 
atOrAbove below identifiers info
--------- ----- ----------- ----
          0.5.0 @{release=0.5.0; summary=cross-site scripting vulnerability} {http://backbonejs.org/#changelog}
 
 
.EXAMPLE
Get-LibraryVulnerabilityInfo Backbone.js -Repository nuget
 
id before infoUri
-- ------ -------
Backbone.js 0.5.3 http://backbonejs.org/#changelog
#>


[CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param(
# The name of the module or package or library to check.
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
[Alias('Module','Package','Library')][string]$Name,
# Whether to check the NPM, JS, or NuGet vulnerability lists.
[ValidateSet('js','npm','nuget')][string]$Repository = 'js'
)
Process
{
    if($Repository -eq 'nuget')
    {
        Invoke-RestMethod https://raw.githubusercontent.com/OWASP/SafeNuGet/master/feed/unsafepackages.xml |
            Select-Xml //package |
            Select-Object -ExpandProperty Node |
            Where-Object {$_.id -eq $Name} |
            ForEach-Object {[pscustomobject]@{id=$_.id;before=$_.before;infoUri=$_.infoUri}}
    }
    else
    {
        $lib = Invoke-RestMethod https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/${Repository}repository.json
        if($lib.$Name) {$lib.$Name.vulnerabilities |Select-Object atOrAbove,below,identifiers,info}
        else {Write-Warning "$Name not found"}
    }
}

}

function Get-NuGetConfigs
{
<#
.SYNOPSIS
Returns the available NuGet configuration files, in order of preference.
 
.OUTPUTS
System.String containing the path to a NuGet config file.
 
.FUNCTIONALITY
Configuration
 
.LINK
https://docs.myget.org/docs/how-to/nuget-configuration-inheritance
 
.EXAMPLE
Get-NuGetConfigs
 
C:\Users\zaphodb\GitHub\ProjectX\src\nuget.config
C:\Users\zaphodb\AppData\Roaming\NuGet\NuGet.config
C:\ProgramData\NuGet\Config.config
C:\ProgramData\NuGet\NuGetDefaults.config
#>


[CmdletBinding()][OutputType([string])] Param(
# The directory to walk the parents of, to look for configs.
[Parameter(Position=0)][string] $Directory = "$PWD"
)

function Get-Parent([Parameter(Position=0)][string] $Directory)
{
    if($Directory -eq "$(Join-Path (Split-Path $Directory -Qualifier) '')") {$Directory}
    else {$Directory; Get-Parent (Split-Path $Directory)}
}

Get-Parent $Directory |ForEach-Object {Join-Path $_ nuget.config} |Where-Object {Test-Path $_ -Type Leaf}
Join-Path $env:APPDATA NuGet NuGet.config |Where-Object {Test-Path $_ -Type Leaf}
Join-Path $env:ProgramData NuGet Config*.config |Resolve-Path -ErrorAction Ignore |Sort-Object Length -Descending
Join-Path $env:ProgramData NuGet NuGetDefaults.config |Where-Object {Test-Path $_ -Type Leaf}

}

function Get-OpenApiInfo
{
<#
.SYNOPSIS
Returns metadata from an OpenAPI definition.
 
.FUNCTIONALITY
Json
 
.LINK
https://www.openapis.org/
 
.EXAMPLE
Get-OpenApiInfo .\test\data\sample-openapi.json
 
Source : .\test\data\sample-openapi.json
OpenApi : 3.0.3
Title : Sample REST API
Description : An example OpenAPI definition.
Version : 1.0.0
Endpoints : {@{Endpoint=GET /users/{userId}; Summary=Returns a user by ID.; Description=Gets a user's details.},
| @{Endpoint=POST /users; Summary=Creates a new user.; Description=Adds a user account.}}
#>


[CmdletBinding()] Param(
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Alias('FullName')][string] $Path
)
Process
{
    Get-Content $Path -Raw |
        ConvertFrom-Json -AsHashtable |
        ForEach-Object {[pscustomobject]@{
            Source      = $Path
            OpenApi     = $_.ContainsKey('openapi') ? $_.openapi :
                $_.ContainsKey('swagger') ? $_.swagger : $null
            Title       = $_.info.title
            Description = $_.info.description
            Version     = $_.info.version
            Endpoints   = $_.paths.GetEnumerator() |% {
                $p = $_.Key
                $e = $_.Value
                $_.Value.Keys |% {
                    [pscustomobject]@{
                        Endpoint    = "$($_.ToUpperInvariant()) $p"
                        Summary     = $e[$_].summary
                        Description = $e[$_].description
                    }
                }
            }
        }}
}

}

function Get-RepoName
{
<#
.SYNOPSIS
Gets the name of the repo.
 
.INPUTS
Objects with System.String Path or FullName properties.
 
.OUTPUTS
System.String of the repo name (the final segment of the first remote location).
 
.FUNCTIONALITY
Git and GitHub
 
.EXAMPLE
Get-RepoName
 
MyRepository
#>


[CmdletBinding()][OutputType([string])] Param(
# The path to the git repo to get the name for.
[Parameter(Position=0,ValueFromPipelineByPropertyName=$true)]
[Alias('FullName')][string] $Path = $PWD.Path
)
Begin
{
    if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
}
Process
{
    if(!(Test-Path $Path -Type Container)) {throw "The path $Path was not found."}
    try
    {
        Push-Location $Path
        git status |Out-Null
        if(!$?) {throw "The path $Path is not a git repo."}
        $remote = git remote |Select-Object -First 1
        if($remote) {return ([uri](git remote get-url $remote)).Segments[-1] -replace '\.git\z',''}
        else {return Split-Path $Path -Leaf}
    }
    finally {Pop-Location}
}

}

function Get-Todos
{
<#
.SYNOPSIS
Returns the TODOs for the current git repo, which can help document technical debt.
 
.EXAMPLE
Get-Todos |Out-GridView -Title "$((Get-Item $(git rev-parse --show-toplevel)).Name) TODOs"
 
Shows TODOs in this repo.
#>


[CmdletBinding()] Param()

Push-Location $(git rev-parse --show-toplevel)
#TODO: Add or replace dependencies
Find-Lines.ps1 -Pattern '\bTODO\b' -Filters * -Path ((Test-Path src -Type Container) ? 'src' : '.') -CaseSensitive |
    ForEach-Object {
        [string[]] $blame = git blame -p -L "$($_.LineNumber),$($_.LineNumber)" -- $_.Path
        $author = $blame |Select-String '^author (?<Author>.*)$' |Select-CapturesFromMatches.ps1 -ValuesOnly
        $Time = $blame |Select-String '^author-time (?<Time>.*)$' |Select-CapturesFromMatches.ps1 -ValuesOnly |
            ConvertFrom-EpochTime.ps1
        [pscustomobject]@{
            Author = $author
            Time = $time
            Todo = ($_.Line -split 'TODO:?\s*',2)[1].Trim()
            Path = Resolve-Path $_.Path -Relative
            LineNumber = $_.LineNumber
        }
    } |
    Sort-Object Time -Descending
Pop-Location

}

function Get-VSCCurrentFile
{
<#
.SYNOPSIS
Returns the path of the current file open in VSCode, when run in the PowerShell Extension Terminal in VSCode.
 
.OUTPUTS
System.String, System.Double, System.Int32, System.Boolean depending on VS Code JSON value type.
 
.FUNCTIONALITY
VSCode
 
.LINK
https://github.com/PowerShell/PowerShellEditorServices
 
.LINK
https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell
 
.EXAMPLE
Get-VSCCurrentFile
 
C:\GitHub\scripts\Get-VSCCurrentFile
#>


[CmdletBinding()][OutputType([string])] Param()
$psEditor.GetEditorContext().CurrentFile.Path

}

function Get-VSCodeSetting
{
<#
.SYNOPSIS
Sets a VSCode setting.
 
.OUTPUTS
System.String, System.Double, System.Int32, System.Boolean depending on VS Code JSON value type.
 
.FUNCTIONALITY
VSCode
 
.LINK
https://code.visualstudio.com/docs/getstarted/settings
 
.LINK
Get-VSCodeSettingsFile
 
.LINK
ConvertFrom-Json
 
.LINK
Get-Content
 
.EXAMPLE
Get-VSCodeSetting /editor.renderWhitespace
 
all
 
.EXAMPLE
Get-VSCodeSetting /powershell.codeFormatting.preset -Workspace
 
Allman
 
.EXAMPLE
Get-VSCodeSetting /prettier.disableLanguages -Workspace
 
markdown
#>


[CmdletBinding()][OutputType([string])][OutputType([double])]
[OutputType([int])][OutputType([bool])] Param(
<#
The full path name of the property to set, as a JSON Pointer, which separates each nested
element name with a /, and literal / is escaped as ~1, and literal ~ is escaped as ~0.
#>

[Parameter(Position=0,Mandatory=$true)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~0|~1)*)\z')]
[string] $JsonPointer,
# Indicates that the current workspace settings should be
[switch] $Workspace
)

${settings.json} = Get-VSCodeSettingsFile -Workspace:$Workspace
if(!(Test-Path ${settings.json} -Type Leaf)) {return $null}
#TODO: Add or replace dependency.
return Select-Json.ps1 $JsonPointer -Path ${settings.json}

}

function Get-VSCodeSettingsFile
{
<#
.SYNOPSIS
Gets the path of the VSCode settings.config file.
 
.OUTPUTS
System.String containing the path of the settings.config file.
 
.FUNCTIONALITY
VSCode
 
.LINK
https://code.visualstudio.com/docs/getstarted/settings
 
.LINK
https://powershell.github.io/PowerShellEditorServices/api/Microsoft.PowerShell.EditorServices.Extensions.EditorObject.html
 
.LINK
https://git-scm.com/docs/git-rev-parse
 
.LINK
Join-Path
 
.LINK
Get-Command
 
.EXAMPLE
Get-VSCodeSettingsFile
 
C:\Users\zaphodb\AppData\Roaming\Code\User\settings.json
 
.EXAMPLE
Get-VSCodeSettingsFile -Workspace
 
C:\Users\zaphodb\GitHub\scripts\.vscode\settings.json
#>


[CmdletBinding()][OutputType([string])] Param(
# Indicates that the current workspace settings should be parsed instead of the user settings.
[switch] $Workspace
)

${settings.json} =
    if($Workspace)
    {
        if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
        if(Get-Variable psEditor -Scope Global -ErrorAction Ignore)
        {
            Join-Path $psEditor.Workspace.Path .vscode/settings.json
        }
        elseif ((Get-Command git -ErrorAction Ignore) -and "$(git rev-parse --git-dir)")
        {
            Join-Path "$(git rev-parse --show-toplevel)" .vscode/settings.json
        }
        else
        {
            Write-Warning "Can't detect VSCode workspace or git repo. Assuming current directory."
            Join-Path "$PWD" .vscode/settings.json
        }
    }
    else
    {
        if(!(Test-Path variable:IsWindows))
        {
            Set-Variable IsWindows ($PSVersionTable.PSEdition -eq 'Desktop' -or  $env:OS -eq 'Windows_NT')
        }
        if($IsWindows)
        {
            # could also try Resolve-Path "$env:APPDATA\Code*\User\settings.json", but this is better targetted
            "$env:APPDATA\Code - Insiders\User\settings.json","$env:APPDATA\Code\User\settings.json" |
                Where-Object {Test-Path $_ -PathType Leaf} |
                Select-Object -First 1
        }
        elseif($IsLinux)
        {
            "$HOME/.config/Code/User/settings.json"
        }
        elseif($IsMacOS)
        {
            "$HOME/Library/Application Support/Code/User/settings.json"
        }
        else
        {
            throw 'Unable to determine location of VSCode settings.json'
        }
    }
Write-Verbose "Using VSCode settings ${settings.json}"

${settings.json}

}

function Import-VsCodeDatabaseConnections
{
<#
.SYNOPSIS
Adds config XDT connection strings to VSCode settings.
 
.FUNCTIONALITY
VSCode
 
.LINK
https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql
 
.LINK
http://code.visualstudio.com/
 
.LINK
https://git-scm.com/docs/git-rev-parse
 
.LINK
Add-VsCodeDatabaseConnection
 
.LINK
Get-ConfigConnectionStringBuilders
 
.EXAMPLE
Import-VsCodeDatabaseConnections
 
Adds any new (by name) connection strings found in XDT .config files into
the .vscode/settings.json mssql.connections collection for the mssql extension.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','',
Justification='All configurations are returned as a collection.')]
[CmdletBinding()][OutputType([void])] Param()

function Get-ConfigConnections
{
    $connections = @()
    foreach($config in (Get-ChildItem -Recurse -Filter *.config |Select-Object -ExpandProperty FullName))
    {
        #TODO: Add or replace dependency.
        foreach($cs in (Get-ConfigConnectionStringBuilders.ps1 $config))
        {
            $name = $cs.Name + '.' + ($config |Split-Path -LeafBase |Split-Path -Extension).Trim('.')
            if($connections -and $name -in $connections.profileName){Write-Verbose "A '$name' connection already exists."; continue}
            #TODO: Add or replace dependency.
            Import-Variables.ps1 $cs.ConnectionString
            $vsconn = @{
                ProfileName = $name
                ServerInstance = ${Data Source}
                Database = ${Data Source}
            }
            if(!${Integrated Security}) {$vsconn += @{UserName=${User ID}}}
            Write-Verbose "Found '$($vsconn.ProfileName)' in $config"
            $connections += [pscustomobject]$vsconn
        }
    }
    Write-Verbose "Found $($connections.Count) connection strings."
    $connections
}

$connections = Get-ConfigConnections
if(!$connections){Write-Verbose 'Nothing to do.'}
else{$connections |Add-VsCodeDatabaseConnection}

}

function New-Jwt
{
<#
.SYNOPSIS
Generates a JSON Web Token (JWT)
 
.OUTPUTS
System.String of an encoded, signed JWT
 
.FUNCTIONALITY
Data formats
 
.LINK
https://tools.ietf.org/html/rfc7519
 
.LINK
https://jwt.io/
 
.LINK
https://docs.microsoft.com/dotnet/api/system.security.cryptography.hmac
 
.EXAMPLE
New-Jwt -Subject 1234567890 -IssuedAt 2018-01-18T01:30:22Z -Secret (ConvertTo-SecureString swordfish -AsPlainText -Force)
 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.59noQVrGQKetFM3RRTe9m4MVBUMkLo3WxqqpPf1xJ-U
#>


[CmdletBinding()][OutputType([string])] Param(
# A hash of JWT body elements.
[ValidateNotNull()][System.Collections.IDictionary] $Body = @{},
# Custom headers (beyond typ and alg) to add to the JWT.
[ValidateNotNull()][System.Collections.IDictionary] $Headers = @{},
# A secret used to sign the JWT.
[Parameter(Mandatory=$true)][ValidateNotNull()][securestring] $Secret,
# The hashing algorithm class to use when signing the JWT.
[ValidateSet('HS256','HS384','HS512')][ValidateNotNull()][string] $Algorithm = 'HS256',
# When the JWT becomes valid.
[datetime] $NotBefore,
# Specifies when the JWT was issued.
[datetime] $IssuedAt,
# Indicates the issued time should be included, based on the current datetime (ignored if IssuedAt is provided).
[switch] $IncludeIssuedAt,
# When the JWT expires.
[datetime] $ExpirationTime,
# How long from now until the JWT expires (ignored if ExpirationTime is provided).
[timespan] $ExpiresAfter,
# A unique (at least within a given issuer) identifier for the JWT.
[ValidateNotNullOrEmpty()][string] $JwtId,
# A string or URI (if it contains a colon) indicating the entity that issued the JWT.
[ValidateNotNullOrEmpty()][ValidateScript({if($_.Contains(':')){return Test-Uri $_}else{$true}})][string] $Issuer,
# The principal (user) of the JWT as a string or URI (if it contains a colon).
[ValidateNotNullOrEmpty()][ValidateScript({if($_.Contains(':')){return Test-Uri $_}else{$true}})][string] $Subject,
# A string or URI (if it contains a colon), or a list of string or URIs that indicates who the JWT is intended for.
[ValidateScript({foreach($s in $_){if($s.Contains(':') -and !(Test-Uri $s)){return $false}};return $true})]
[ValidateCount(1,2147483647)][System.String[]] $Audience,
# Additional claims to add to the body of the JWT.
[hashtable] $Claims = @{}
)
#TODO: Add or replace dependencies.
function ConvertTo-JSON64($o) {ConvertTo-Base64 ([Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $o -Compress))) -UriStyle}
function ConvertTo-NumericDate([datetime]$d)
{
    if($d.Kind -eq 'Utc') {[int](Get-Date $d -UFormat %s)}
    else {[int](Get-Date $d.ToUniversalTime() -UFormat %s)}
}
function ConvertFrom-NumericDate([int]$i)
{
    if(!$i) {return}
    (New-Object datetime 1970,1,1,0,0,0,([DateTimeKind]::Utc)).AddSeconds($i).ToLocalTime()
}

$Headers['alg'] = $Algorithm
$Headers['typ'] = 'JWT'
if($NotBefore) {$Body['nbf'] = ConvertTo-NumericDate $NotBefore}
if($IncludeIssuedAt) {$Body['iat'] = ConvertTo-NumericDate (Get-Date)}
elseif($IssuedAt) {$Body['iat'] = ConvertTo-NumericDate $IssuedAt}
if($ExpirationTime) {$Body['exp'] = ConvertTo-NumericDate $ExpirationTime}
elseif($ExpiresAfter) {$Body['exp'] = ConvertTo-NumericDate ((Get-Date).Add($ExpiresAfter))}
'iat','nbf','exp' |ForEach-Object {Set-Variable $_ $(if($Body.ContainsKey($_)){ConvertFrom-NumericDate $Body[$_]}else{'whenever'})}
Write-Verbose "JWT issued $iat valid $nbf to $exp"
if($JwtId) {$Body['jti'] = $JwtId}
if($Issuer) {$Body['iss'] = $Issuer}
if($Subject) {$Body['sub'] = $Subject}
if($Audience) {$Body['aud'] = $Audience}
$Body += $Claims
Write-Verbose "JWT headers: $(ConvertTo-Json $Headers -Compress)"
Write-Verbose "JWT body: $(ConvertTo-Json $Body -Compress)"
$jwt = "$(ConvertTo-JSON64 $Headers).$(ConvertTo-JSON64 $Body)"
Write-Verbose "Unsigned JWT: $jwt"
$secred = New-Object pscredential 'secret',$Secret
[byte[]]$secbytes = [Text.Encoding]::UTF8.GetBytes(($secred.Password |ConvertFrom-SecureString -AsPlainText))
$hash = New-Object "Security.Cryptography.$($Algorithm -replace '\AHS','HMACSHA')" (,$secbytes)
$secbytes = $null
Write-Verbose "Signing JWT with $($hash.GetType().Name)"
$jwt = "$jwt.$(ConvertTo-Base64 ($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($jwt))) -UriStyle)"
$hash.Dispose()
$jwt

}

function Push-WorkspaceLocation
{
<#
.SYNOPSIS
Pushes the current VS Code editor workspace location to the location stack.
 
.FUNCTIONALITY
VSCode
 
.LINK
Push-Location
 
.EXAMPLE
Push-WorkspaceLocation
 
Pushes the current directory onto the stack, and changes to the workspace directory.
#>


[CmdletBinding()][OutputType([void])] Param()
#TODO: Add or replace dependency.
if(Test-Variable.ps1 psEditor) {Push-Location $psEditor.Workspace.Path}
else {throw 'Missing psEditor object'}

}

function Rename-GitHubLocalBranch
{
<#
.SYNOPSIS
Rename a git repository branch.
 
.FUNCTIONALITY
Git and GitHub
 
.LINK
https://docs.github.com/en/github/administering-a-repository/managing-branches-in-your-repository/renaming-a-branch#updating-a-local-clone-after-a-branch-name-changes
 
.LINK
https://github.com/github/renaming
 
.EXAMPLE
Rename-GitHubLocalBranch main
 
Rename the current branch to "main".
#>


[CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param(
# The new branch name.
[Parameter(Position=0,Mandatory=$true)][string] $NewName
)

if(!$PSCmdlet.ShouldContinue('Have you renamed the branch in the GitHub UI?','GitHub Status'))
{
    Write-Information 'Rename the branch via the GitHub UI before running this script.'
    if((Get-Command gh -CommandType Application -ErrorAction Ignore)) {gh browse}
    Start-Process 'https://docs.github.com/en/github/administering-a-repository/managing-branches-in-your-repository/renaming-a-branch#renaming-a-branch'
    return
}

$oldName = git rev-parse --abbrev-ref HEAD
if(!$PSCmdlet.ShouldProcess("$oldName branch","rename to $NewName")) {return}

if(!(Get-Command git -Type Application -ErrorAction Ignore)) {throw "Git is required to be installed!"}
Write-Verbose "git branch -m $oldName $NewName"
git branch -m $oldName $NewName
Write-Verbose "git fetch origin"
git fetch origin
Write-Verbose "git branch -u origin/$NewName $NewName"
git branch -u "origin/$NewName" $NewName
Write-Verbose 'git remote set-head origin -a'
git remote set-head origin -a
Write-Verbose 'git remote prune origin'
git remote prune origin

}

function Repair-ScriptStyle
{
<#
.SYNOPSIS
Accepts justifications for script analysis rule violations, fixing the rest using Invoke-ScriptAnalysis.
 
.FUNCTIONALITY
Scripts
 
.LINK
https://docs.microsoft.com/powershell/module/psscriptanalyzer/invoke-scriptanalyzer
 
.EXAMPLE
Repair-ScriptStyle .\MyScript.ps1
 
 PSAvoidUsingWriteHost in A:\Scripts\MyScript.ps1
 (!) Warning
 Lines: 19, 24, 25, 26, 27, 31, 32
 File 'MyScript.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts,
does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected.
Instead, use Write-Output, Write-Verbose, or Write-Information.
 
Confirm
Are you sure you want to perform this action?
Performing the operation "provide justification" on target "PSAvoidUsingWriteHost in A:\Scripts\MyScript.ps1".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"):
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost','',
Justification='This script is not intended for pipeline redirection. Also, it uses color.')]
[CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param(
# The path to a PowerShell script file to repair the style of.
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[Alias('FullName')][string] $Path
)
Process
{
    $suppress = @()
    foreach($rule in Invoke-ScriptAnalyzer $Path |Group-Object RuleName)
    {
        $name = $rule.Name
        Write-Information " $name in $Path "
        foreach($severity in $rule.Group |Group-Object Severity)
        {
            switch($severity.Name)
            {
                Information {Write-Information ' 🆗 Information '}
                Warning {Write-Information ' ⚠️ Warning '}
                Error {Write-Information ' ❌ Error '}
                default {Write-Information " $($severity.Name) "}
            }
            foreach($message in $severity.Group |Group-Object Message)
            {
                Write-Information " Lines: $($message.Group.Line -join ', ')"
                Write-Information " $($message.Name)"
            }
        }
        if(!$PSCmdlet.ShouldProcess("$name in $Path",'provide justification')) {continue}
        $suppress += @"
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('$name','',
Justification='$((Read-Host 'Justification') -replace "'","''")')]
 
"@

    }
    if($suppress)
    {
        (Get-Content $Path -Raw) -replace '(?m)^(\[CmdletBinding\b)',"$suppress`$1" |
            ForEach-Object {$_.Trim()} |
            Out-File $Path utf8BOM
    }
    Invoke-ScriptAnalyzer $Path -Fix
}

}

function Set-VSCodeSetting
{
<#
.SYNOPSIS
Sets a VSCode setting.
 
.FUNCTIONALITY
VSCode
 
.LINK
https://code.visualstudio.com/docs/getstarted/settings
 
.LINK
Get-VSCodeSettingsFile
 
.EXAMPLE
Set-VSCodeSetting git.autofetch $true -Workspace
 
Sets {"git.autofetch": true} in the VSCode user settings.
 
.EXAMPLE
Set-VSCodeSetting /powershell.codeFormatting.preset Allman -Workspace
 
Sets {"powershell.codeFormatting.preset": "Allman"} in the VSCode workspace settings.
 
.EXAMPLE
Set-VSCodeSetting /workbench.colorTheme 'PowerShell ISE' -Workspace
 
Sets {"workbench.colorTheme": "PowerShell ISE"} in the VSCode workspace settings.
#>


[CmdletBinding()][OutputType([void])] Param(
<#
The full path name of the property to set, as a JSON Pointer, which separates each nested
element name with a /, and literal / is escaped as ~1, and literal ~ is escaped as ~0.
#>

[Parameter(Position=0,Mandatory=$true)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~0|~1)*)\z')]
[string] $JsonPointer = '',
# The value of the setting to set.
[Parameter(Position=1,Mandatory=$true)][AllowEmptyString()][AllowEmptyCollection()][AllowNull()]
[psobject] $Value,
# Indicates that the current workspace settings should be set, rather than the user settings.
[switch] $Workspace
)

${settings.json} = Get-VSCodeSettingsFile -Workspace:$Workspace

if(!(${settings.json} |Split-Path |Test-Path -PathType Container)) {mkdir (${settings.json} |Split-Path) |Out-Null}
if(!(Test-Path ${settings.json} -PathType Leaf)) {'{}' |Out-File ${settings.json} -Encoding utf8}

$settings = Get-Content ${settings.json} -Raw
#TODO: Add or replace dependency.
$settings |Set-Json.ps1 $JsonPointer $Value -WarnOverwrite |Out-File ${settings.json} -Encoding utf8

}

function Show-DataRef
{
<#
.SYNOPSIS
Display an HTML view of an XML schema or WSDL using Saxon.
 
.FUNCTIONALITY
XML
 
.EXAMPLE
Show-DataRef DataModel.xsd
 
(Renders the XML schema as HTML.)
#>


[CmdletBinding()][OutputType([void])] Param(
# System.String containing the path to an XML Schema or WSDL file.
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][string]$SchemaFile
)
Begin
{
    #TODO: Add or replace dependency.
    Use-Command.ps1 saxon $env:ProgramFiles\Saxonica\Saxon*\bin\Transform.exe -nupkg saxon-he `
        -InstallDir $env:ProgramFiles\Saxonica
}
Process
{
    $css  = Join-Path $PSScriptRoot 'dataref.css'
    $xslt = Join-Path $PSScriptRoot 'dataref.xslt'
    $html = [IO.Path]::ChangeExtension($SchemaFile,'html')
    Copy-Item $css .
    C:\ProgramData\chocolatey\bin\SaxonHE\bin\Transform.exe -s:$SchemaFile -xsl:$xslt -o:$html
    Invoke-Item $html
}

}

function Show-OpenApiInfo
{
<#
.SYNOPSIS
Displays metadata from an OpenAPI definition.
 
.FUNCTIONALITY
Json
 
.LINK
https://www.openapis.org/
 
.EXAMPLE
Show-OpenApiInfo .\test\data\sample-openapi.json
 
Sample REST API v1.0.0 An example OpenAPI definition.
.\test\data\sample-openapi.json openapi v3.0.3
GET /users/{userId} Returns a user by ID.
Gets a user's details.
POST /users Creates a new user.
Adds a user account.
#>


[CmdletBinding()] Param(
[Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Alias('FullName')][string] $Path
)
Process
{
    $api = Get-OpenApiInfo $Path
    Write-Host $api.Title -ForegroundColor Green -NoNewline
    Write-Host " $($api.Version) " -ForegroundColor DarkCyan -NoNewline
    if($api.Description) {Write-Host $api.Description -ForegroundColor DarkGreen}
    Write-Host $api.Source -ForegroundColor Cyan -NoNewline
    Write-Host " OpenAPI v$($api.OpenApi)" -ForegroundColor DarkCyan
    foreach($endpoint in $api.Endpoints)
    {
        Write-Host $endpoint.Endpoint -ForegroundColor White -NoNewline
        Write-Host ' # ' -ForegroundColor DarkGreen -NoNewline
        Write-Host $endpoint.Summary -ForegroundColor DarkGreen
        Write-Host $endpoint.Description -ForegroundColor DarkGray
    }
}

}

function Test-Jwt
{
<#
.SYNOPSIS
Determines whether a string is a valid JWT.
 
.INPUTS
System.String value to test for a valid URI format.
 
.OUTPUTS
System.Boolean indicating that the string can be parsed as a URI.
 
.FUNCTIONALITY
Data formats
 
.EXAMPLE
Test-Jwt 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOjE1MTYyMzkwMjIsInN1YiI6IjEyMzQ1Njc4OTAifQ.-zAn1et1mf6QHakJbOTt5-p4gv33R7cIikKy8-9aiNs' (ConvertTo-SecureString swordfish -AsPlainText -Force)
 
True
#>


[CmdletBinding()][OutputType([bool])] Param(
# The string to test.
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][AllowEmptyString()][AllowNull()][string] $InputObject,
# The secret used to sign the JWT.
[Parameter(Position=1,Mandatory=$true)][SecureString] $Secret
)
Process
{
    if(!$InputObject) {Write-Verbose 'No JWT input'; return $false}
    if(!$InputObject.Contains([char]'.')) {Write-Verbose 'JWT is missing a dot'; return $false}
    $head64,$body64,$sign64 = $InputObject -split '\.'
    #TODO: Add or replace dependency.
    $head = ConvertFrom-Base64.ps1 $head64 utf8 -UriStyle
    Write-Verbose "JWT head: $head"
    if(!(Test-Json $head)) {Write-Verbose 'JWT header does not decode to valid JSON'; return $false}
    $head = ConvertFrom-Json $head
    if($head.typ -ne 'JWT') {Write-Verbose "JWT type is $($head.typ)"; return $false}
    if($head.alg -notin 'HS256','HS384','HS512') {Write-Verbose "Unsupported algorithm: $($head.alg)"; return $false}
    $body = ConvertFrom-Base64.ps1 $body64 utf8 -UriStyle
    Write-Verbose "JWT body: $body"
    if(!(Test-Json $body)) {Write-Verbose 'JWT body does not decode to valid JSON'; return $false}
    $body = ConvertFrom-Json $body
    [byte[]]$sign = ConvertFrom-Base64.ps1 $sign64 -UriStyle
    $secred = New-Object pscredential 'secret',$Secret
    [byte[]]$secbytes = [Text.Encoding]::UTF8.GetBytes(($secred.Password |ConvertFrom-SecureString -AsPlainText))
    $hash = New-Object "Security.Cryptography.$($head.alg -replace '\AHS','HMACSHA')" (,$secbytes)
    if(Compare-Object $sign ($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes("$head64.$body64"))))
    {Write-Verbose "JWT hashes do not match"; return $false}
    return $true
}

}

function Trace-GitRepoTest
{
<#
.SYNOPSIS
Uses git bisect to search for the point in the repo history that the test script starts returning true.
 
.FUNCTIONALITY
Git and GitHub
 
.EXAMPLE
Trace-GitRepoTest { dotnet build; !$? }
 
Searches the full repo history for the point at which the build broke.
#>


[CmdletBinding()] Param(
# A script block that returns a boolean corresponding to a state introduced at some point in the repo history.
[Parameter(Position=0,Mandatory=$true)][ScriptBlock] $TestScript,
# A commit from the repo history without the new state.
[Parameter(Position=1)][string] $GoodCommit = $(Get-GitFirstCommit),
# A commit from the repo history with the new state.
[Parameter(Position=2)][string] $BadCommit = $(git rev-parse HEAD)
)
git bisect start |Write-Verbose
git bisect good $GoodCommit |Write-Verbose
git bisect bad $BadCommit |Write-Verbose
do
{
    $state = if($TestScript.Invoke()) {'bad'} else {'good'}
    $result = git bisect $state
    $result |Write-Verbose
}
while($result |Select-String '\ABisecting: ')
git bisect reset |Write-Verbose
return $result

}
Export-ModuleMember -Function Add-GitHubMetadata,Add-NotebookCell,Add-VsCodeDatabaseConnection,Copy-GitHubLabels,Export-OpenApiSchema,Find-DotNetTools,Get-AssemblyFramework,Get-GitFileMetadata,Get-GitFirstCommit,Get-GitHubRepoChildItem,Get-GitRepoChangeStatus,Get-LibraryVulnerabilityInfo,Get-NuGetConfigs,Get-OpenApiInfo,Get-RepoName,Get-Todos,Get-VSCCurrentFile,Get-VSCodeSetting,Get-VSCodeSettingsFile,Import-VsCodeDatabaseConnections,New-Jwt,Push-WorkspaceLocation,Rename-GitHubLocalBranch,Repair-ScriptStyle,Set-VSCodeSetting,Show-DataRef,Show-OpenApiInfo,Test-Jwt,Trace-GitRepoTest