# 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 }