Files
RoslynBridge/rb.ps1
AJ Isaacs daa60c53dd chore(scripts): add rb.ps1 helper for Roslyn Bridge queries
Auto-detects Web API (5001) or VS plugin (59123), prefers compact JSON output,
adds commands for projects, diagnostics, symbol/ref lookup, history, and
solution-overview summary.
2025-10-28 18:18:44 -04:00

373 lines
15 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roslyn Bridge PowerShell helper
#
# Purpose: Fast, lowtoken wrapper around the Roslyn Bridge Web API (port 5001)
# and the legacy VS plugin (port 59123). Auto-detects which is running
# and provides concise commands for common operations.
#
# Usage examples:
# .\rb.ps1 health
# .\rb.ps1 projects
# .\rb.ps1 overview
# .\rb.ps1 diagnostics -FilePath C:/path/to/File.cs
# .\rb.ps1 symbol -FilePath C:/path/to/File.cs -Line 12 -Column 4
# .\rb.ps1 references -FilePath C:/path/to/File.cs -Line 12 -Column 4
# .\rb.ps1 search -SymbolName MyService -Kind class
# .\rb.ps1 build -ProjectName MyProject
# .\rb.ps1 package-add -ProjectName MyProject -PackageName Newtonsoft.Json -Version 13.0.3
# .\rb.ps1 history-recent -Count 10
# .\rb.ps1 query -BodyJson '{"queryType":"searchcode","symbolName":".*Controller"}'
#
# Output:
# - Compressed JSON by default (minimal tokens)
# - Add -Pretty for indented JSON
# - Add -Raw to return the raw response string
[CmdletBinding(PositionalBinding=$true)]
param(
[Parameter(Position=0, Mandatory=$true)]
[ValidateSet(
'health','projects','overview','diagnostics','symbol','references','search',
'build','package-add','history','history-recent','history-stats',
'plugin-health','solution-overview'
)]
[string]$Command,
# Common params
[string]$BaseUrl, # Overrides auto-detected base URL
[switch]$NoDetect, # Skip auto-detection
[int]$TimeoutSec = 6, # Short default to stay snappy
[switch]$Raw, # Emit raw response string
[switch]$Pretty, # Indented JSON output
# Query params
[string]$FilePath,
[int]$Line,
[int]$Column,
[string]$SymbolName,
[ValidateSet('class','method','property','field','interface','enum','struct','namespace','event','any')]
[string]$Kind = 'any',
# Project ops
[string]$ProjectName,
[string]$Configuration,
[string]$PackageName,
[string]$Version,
# History
[int]$Count = 25,
# Solution overview summary options
[ValidateSet('json','text','yaml')]
[string]$Format = 'json',
[int]$Top = 5,
[bool]$IncludeNamespaces = $true
)
set-strictmode -version latest
$ErrorActionPreference = 'Stop'
function Write-Err {
param([string]$Msg)
Write-Error -Message $Msg
}
function Get-JsonOutput {
param(
[Parameter(Mandatory=$true)] $Data
)
if ($Raw) {
# If we were given an object, compress to a compact string; if it's already a string, emit it.
if ($Data -is [string]) { return $Data }
return ($Data | ConvertTo-Json -Depth 50 -Compress)
}
$json = $Data | ConvertTo-Json -Depth 50 -Compress
if ($Pretty) { $json = ($Data | ConvertTo-Json -Depth 50) }
return $json
}
function Test-Url {
param(
[string]$Url,
[ValidateSet('GET','POST')][string]$Method = 'GET'
)
try {
if ($Method -eq 'GET') {
$resp = Invoke-WebRequest -Method GET -Uri $Url -TimeoutSec $TimeoutSec
} else {
$resp = Invoke-WebRequest -Method POST -Uri $Url -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body '{}'
}
return $true
} catch {
return $false
}
}
function Get-BridgeEndpoints {
# Decide which base URL to use.
# Priority: explicit -BaseUrl -> Web API (5001) if healthy -> Plugin (59123) if healthy
$web = 'http://localhost:5001'
$plugin = 'http://localhost:59123'
if ($BaseUrl) {
return @{ BaseUrl = $BaseUrl; Mode = 'explicit' }
}
if ($NoDetect) {
return @{ BaseUrl = $web; Mode = 'web' }
}
if (Test-Url -Url "$web/api/health/ping" -Method GET) {
return @{ BaseUrl = $web; Mode = 'web' }
}
if (Test-Url -Url "$plugin/health" -Method POST) {
return @{ BaseUrl = $plugin; Mode = 'plugin' }
}
# Fallback to web
return @{ BaseUrl = $web; Mode = 'web' }
}
function Invoke-WebApi {
param(
[string]$BaseUrl,
[ValidateSet('GET','POST')][string]$Method,
[string]$Path,
[hashtable]$Query,
$Body
)
$uri = [System.UriBuilder]::new($BaseUrl)
if ($Path.StartsWith('/')) { $uri.Path = $Path } else { $uri.Path = "$($uri.Path.TrimEnd('/'))/$Path" }
if ($Query) {
$qs = [System.Web.HttpUtility]::ParseQueryString([string]::Empty)
foreach ($k in $Query.Keys) {
if ($null -ne $Query[$k] -and $Query[$k] -ne '') { $qs[$k] = [string]$Query[$k] }
}
$uri.Query = $qs.ToString()
}
if ($Raw) {
if ($Method -eq 'GET') {
return (Invoke-WebRequest -Method GET -Uri $uri.Uri -TimeoutSec $TimeoutSec).Content
} else {
$bodyStr = if ($Body -is [string]) { $Body } else { ($Body | ConvertTo-Json -Depth 50 -Compress) }
return (Invoke-WebRequest -Method POST -Uri $uri.Uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body $bodyStr).Content
}
}
if ($Method -eq 'GET') {
return Invoke-RestMethod -Method GET -Uri $uri.Uri -TimeoutSec $TimeoutSec
} else {
if ($Body -is [string]) {
return Invoke-RestMethod -Method POST -Uri $uri.Uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body $Body
} else {
return Invoke-RestMethod -Method POST -Uri $uri.Uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body ($Body | ConvertTo-Json -Depth 50 -Compress)
}
}
}
function Invoke-PluginApi {
param(
[string]$BaseUrl,
[string]$Path, # '/query' or '/health'
$Body # hashtable or string
)
$uri = "$BaseUrl$Path"
if ($Raw) {
$bodyStr = if ($Body -is [string]) { $Body } else { ($Body | ConvertTo-Json -Depth 50 -Compress) }
return (Invoke-WebRequest -Method POST -Uri $uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body $bodyStr).Content
}
if ($Body -is [string]) {
return Invoke-RestMethod -Method POST -Uri $uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body $Body
} else {
return Invoke-RestMethod -Method POST -Uri $uri -TimeoutSec $TimeoutSec -ContentType 'application/json' -Body ($Body | ConvertTo-Json -Depth 50 -Compress)
}
}
#
# Main dispatch
#
$endpoint = Get-BridgeEndpoints
$mode = $endpoint.Mode
$base = $endpoint.BaseUrl
try {
switch ($Command) {
'health' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/health'
$out = Get-JsonOutput -Data $resp
Write-Output $out
} else {
$resp = Invoke-PluginApi -BaseUrl $base -Path '/health' -Body '{}'
$out = Get-JsonOutput -Data $resp
Write-Output $out
}
}
'projects' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/projects'
} else {
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body @{ queryType = 'getprojects' }
}
Write-Output (Get-JsonOutput -Data $resp)
}
'overview' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/solution/overview'
} else {
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body @{ queryType = 'getsolutionoverview' }
}
Write-Output (Get-JsonOutput -Data $resp)
}
'diagnostics' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{}
if ($FilePath) { $q.filePath = $FilePath }
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/diagnostics' -Query $q
} else {
$body = @{ queryType = 'getdiagnostics' }
if ($FilePath) { $body.filePath = $FilePath }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'symbol' {
if (-not $FilePath -or -not $PSBoundParameters.ContainsKey('Line') -or -not $PSBoundParameters.ContainsKey('Column')) {
Write-Err 'symbol requires -FilePath, -Line, -Column'; exit 2
}
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ filePath=$FilePath; line=$Line; column=$Column }
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/symbol' -Query $q
} else {
$body = @{ queryType='getsymbol'; filePath=$FilePath; line=$Line; column=$Column }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'references' {
if (-not $FilePath -or -not $PSBoundParameters.ContainsKey('Line') -or -not $PSBoundParameters.ContainsKey('Column')) {
Write-Err 'references requires -FilePath, -Line, -Column'; exit 2
}
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ filePath=$FilePath; line=$Line; column=$Column }
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/references' -Query $q
} else {
$body = @{ queryType='findreferences'; filePath=$FilePath; line=$Line; column=$Column }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'search' {
if (-not $SymbolName) { Write-Err 'search requires -SymbolName'; exit 2 }
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ symbolName=$SymbolName }
if ($Kind -and $Kind -ne 'any') { $q.kind = $Kind }
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/symbol/search' -Query $q
} else {
$parameters = @{}
if ($Kind -and $Kind -ne 'any') { $parameters.kind = $Kind }
$body = @{ queryType='findsymbol'; symbolName=$SymbolName }
if ($parameters.Count -gt 0) { $body.parameters = $parameters }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'build' {
if (-not $ProjectName) { Write-Err 'build requires -ProjectName'; exit 2 }
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ projectName=$ProjectName }
if ($Configuration) { $q.configuration = $Configuration }
$resp = Invoke-WebApi -BaseUrl $base -Method POST -Path '/api/roslyn/project/build' -Query $q
} else {
$body = @{ queryType='buildproject'; projectName=$ProjectName }
if ($Configuration) { $body.configuration = $Configuration }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'package-add' {
if (-not $ProjectName -or -not $PackageName) { Write-Err 'package-add requires -ProjectName and -PackageName'; exit 2 }
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ projectName=$ProjectName; packageName=$PackageName }
if ($Version) { $q.version = $Version }
$resp = Invoke-WebApi -BaseUrl $base -Method POST -Path '/api/roslyn/project/package/add' -Query $q
} else {
$body = @{ queryType='addnugetpackage'; projectName=$ProjectName; packageName=$PackageName }
if ($Version) { $body.version = $Version }
$resp = Invoke-PluginApi -BaseUrl $base -Path '/query' -Body $body
}
Write-Output (Get-JsonOutput -Data $resp)
}
'history' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/history'
} else {
Write-Err 'history is only available via Web API (port 5001).'; exit 2
}
Write-Output (Get-JsonOutput -Data $resp)
}
'history-recent' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$q = @{ count = $Count }
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/history/recent' -Query $q
} else {
Write-Err 'history-recent is only available via Web API (port 5001).'; exit 2
}
Write-Output (Get-JsonOutput -Data $resp)
}
'history-stats' {
if ($mode -eq 'web' -or $mode -eq 'explicit') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/history/stats'
} else {
Write-Err 'history-stats is only available via Web API (port 5001).'; exit 2
}
Write-Output (Get-JsonOutput -Data $resp)
}
'plugin-health' {
$pluginBase = if ($BaseUrl) { $BaseUrl } else { 'http://localhost:59123' }
$resp = Invoke-PluginApi -BaseUrl $pluginBase -Path '/health' -Body '{}'
Write-Output (Get-JsonOutput -Data $resp)
}
'solution-overview' {
if ($mode -ne 'web' -and $mode -ne 'explicit') {
Write-Err 'solution-overview requires the Web API (port 5001).'; exit 2
}
$q = @{ topNProjects=$Top; includeNamespaces=$IncludeNamespaces; format=$Format }
# For text/yaml formats, we want the raw response as-is
if ($Format -eq 'json') {
$resp = Invoke-WebApi -BaseUrl $base -Method GET -Path '/api/roslyn/solution/overview/summary' -Query $q
Write-Output (Get-JsonOutput -Data $resp)
} else {
# Build query string properly
$qs = [System.Web.HttpUtility]::ParseQueryString([string]::Empty)
$qs['topNProjects'] = [string]$Top
$qs['includeNamespaces'] = [string]$IncludeNamespaces
$qs['format'] = $Format
$uri = [System.UriBuilder]::new("$base/api/roslyn/solution/overview/summary")
$uri.Query = $qs.ToString()
$content = (Invoke-WebRequest -Method GET -Uri $uri.Uri -TimeoutSec $TimeoutSec).Content
Write-Output $content
}
}
default {
Write-Err "Unknown command: $Command"; exit 2
}
}
} catch {
$msg = $_.Exception.Message
$respProp = $_.Exception.PSObject.Properties['Response']
if ($respProp -and $respProp.Value) {
try {
$resp = $respProp.Value
$stream = $resp.GetResponseStream()
if ($stream) {
$reader = New-Object System.IO.StreamReader($stream)
$content = $reader.ReadToEnd()
if ($content) { $msg = "$msg`n$content" }
}
} catch {}
}
Write-Err $msg
exit 1
}