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.
This commit is contained in:
372
rb.ps1
Normal file
372
rb.ps1
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# Roslyn Bridge PowerShell helper
|
||||||
|
#
|
||||||
|
# Purpose: Fast, low‑token 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user