vscode-qbuild/install.ps1
|
<#
.SYNOPSIS Install the qbuild Map Tools extension from GitHub releases. .DESCRIPTION Downloads the VSIX for a qbuild-map-tools release and installs it with the VS Code, Cursor, or VS Code Insiders CLI. Pipe the script to Invoke-Expression: iwr -UseBasicParsing https://raw.githubusercontent.com/qbikez/ps-publishmap/master/src/configmap/vscode-qbuild/install.ps1 | iex Set QBUILD_EDITOR to code, cursor, or code-insiders to install into one editor. Set QBUILD_MAP_TOOLS_VERSION to a major.minor.patch version to pin a release. When those variables are empty, the latest release is installed into every detected editor. .EXAMPLE iwr -UseBasicParsing https://raw.githubusercontent.com/qbikez/ps-publishmap/master/src/configmap/vscode-qbuild/install.ps1 | iex .EXAMPLE $env:QBUILD_EDITOR = 'cursor' $env:QBUILD_MAP_TOOLS_VERSION = '0.1.1' ./install.ps1 .EXAMPLE . ./install.ps1 Install-QBuildMapTools -Editor cursor -Version 0.1.1 #> function Get-QBuildMapToolsNextLink { param($Link) $text = @($Link | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ',' if ($text -match '<(?<url>[^>]+)>;\s*rel="next"') { return $Matches.url } return $null } function Select-QBuildMapToolsTag { [CmdletBinding()] param( [AllowEmptyCollection()] [object[]]$Refs = @(), [string]$Version ) if (-not [string]::IsNullOrWhiteSpace($Version) -and $Version -notmatch '^\d+\.\d+\.\d+$') { throw "Version '$Version' must be major.minor.patch." } $parsed = @( foreach ($ref in @($Refs)) { $name = [string]$ref.ref if ($name -match '^refs/tags/qbuild-map-tools-v(?<version>\d+\.\d+\.\d+)$') { [pscustomobject]@{ Tag = "qbuild-map-tools-v$($Matches.version)" Version = [version]$Matches.version } } } ) if (-not [string]::IsNullOrWhiteSpace($Version)) { $match = $parsed | Where-Object { $_.Version -eq [version]$Version } | Select-Object -First 1 if ($null -eq $match) { $published = @($parsed | Sort-Object Version -Descending | ForEach-Object { $_.Version.ToString() }) $available = if ($published.Count -eq 0) { 'none' } else { $published -join ', ' } throw "qbuild Map Tools version '$Version' was not found. Published versions: $available." } return $match } $selected = $parsed | Sort-Object Version -Descending | Select-Object -First 1 if ($null -eq $selected) { throw 'No qbuild Map Tools release was found.' } return $selected } function Select-QBuildMapToolsAsset { param($Release) $tag = [string]$Release.tag_name if ($tag -notmatch '^qbuild-map-tools-v(?<version>\d+\.\d+\.\d+)$') { throw "Release tag '$tag' is not a qbuild Map Tools release." } $version = [version]$Matches.version $asset = @($Release.assets) | Where-Object { $_.name -like '*.vsix' } | Select-Object -First 1 if ($null -eq $asset -or [string]::IsNullOrWhiteSpace([string]$asset.browser_download_url)) { throw "Release '$tag' does not include a VSIX asset." } [pscustomobject]@{ Version = $version Tag = $tag AssetName = [string]$asset.name DownloadUrl = [string]$asset.browser_download_url } } function Get-QBuildMapToolsEditorPath { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name, [string]$LocalAppData = $env:LOCALAPPDATA, [string]$ProgramFiles = $env:ProgramFiles ) $relative = switch ($Name) { 'code' { @( @{ Root = $LocalAppData; Relative = 'Programs\Microsoft VS Code\bin\code.cmd' } @{ Root = $ProgramFiles; Relative = 'Microsoft VS Code\bin\code.cmd' } ) } 'code-insiders' { @( @{ Root = $LocalAppData; Relative = 'Programs\Microsoft VS Code Insiders\bin\code-insiders.cmd' } @{ Root = $ProgramFiles; Relative = 'Microsoft VS Code Insiders\bin\code-insiders.cmd' } ) } 'cursor' { @( @{ Root = $LocalAppData; Relative = 'Programs\cursor\resources\app\bin\cursor.cmd' } @{ Root = $LocalAppData; Relative = 'Programs\Cursor\resources\app\bin\cursor.cmd' } ) } default { throw "Unknown editor '$Name'. Use code, cursor, or code-insiders." } } foreach ($entry in $relative) { if (-not [string]::IsNullOrWhiteSpace([string]$entry.Root)) { Join-Path -Path $entry.Root -ChildPath $entry.Relative } } } function Resolve-QBuildMapToolsEditor { [CmdletBinding()] param( [string]$Editor, [string]$LocalAppData = $env:LOCALAPPDATA, [string]$ProgramFiles = $env:ProgramFiles, [scriptblock]$GetCommand = { param($Name) $command = Get-Command -Name $Name -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -eq $command) { return $null } return $command.Source }, [scriptblock]$TestEditorPath = { param($Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } Test-Path -LiteralPath $Path } ) $allowed = @('code', 'cursor', 'code-insiders') $names = if ([string]::IsNullOrWhiteSpace($Editor)) { $allowed } else { @($Editor) } $resolved = @( foreach ($name in $names) { if ($name -notin $allowed) { throw "Unknown editor '$name'. Use code, cursor, or code-insiders." } $command = & $GetCommand $name if ($command -and $command -notmatch '(?i)\\WindowsApps\\') { [pscustomobject]@{ Name = $name Command = [string]$command } continue } $path = @(Get-QBuildMapToolsEditorPath -Name $name -LocalAppData $LocalAppData -ProgramFiles $ProgramFiles) | Where-Object { & $TestEditorPath $_ } | Select-Object -First 1 if (-not [string]::IsNullOrWhiteSpace([string]$path)) { [pscustomobject]@{ Name = $name Command = [string]$path } continue } if (-not [string]::IsNullOrWhiteSpace($Editor)) { throw "Editor '$name' was not found. Install the '$name' shell command from the Command Palette, then run this script again." } } ) if ($resolved.Count -eq 0) { throw "No editor CLI was found. Install the 'code', 'cursor', or 'code-insiders' command from the Command Palette, then run this script again." } return $resolved } function Invoke-QBuildMapToolsGitHubGet { param( [Parameter(Mandatory)] [string]$Uri ) if ($PSVersionTable.PSVersion.Major -lt 6) { $protocols = [Net.ServicePointManager]::SecurityProtocol if (($protocols -band [Net.SecurityProtocolType]::Tls12) -eq 0) { [Net.ServicePointManager]::SecurityProtocol = $protocols -bor [Net.SecurityProtocolType]::Tls12 } } $headers = @{ Accept = 'application/vnd.github+json' 'User-Agent' = 'qbuild-map-tools-install' 'X-GitHub-Api-Version' = '2022-11-28' } try { return Invoke-WebRequest -Uri $Uri -Headers $headers -UseBasicParsing } catch { $status = $null $response = $_.Exception.Response if ($null -ne $response) { $status = [int]$response.StatusCode } if ($status -eq 404) { return $null } if ($status -eq 403) { throw "GitHub API rate limit was reached while looking up qbuild Map Tools. Download a VSIX from https://github.com/qbikez/ps-publishmap/releases and install it with: code --install-extension <file>.vsix" } throw } } function Get-QBuildMapToolsTagRef { $uri = 'https://api.github.com/repos/qbikez/ps-publishmap/git/matching-refs/tags/qbuild-map-tools-v?per_page=100' $refs = @() for ($page = 0; $uri -and $page -lt 10; $page++) { $response = Invoke-QBuildMapToolsGitHubGet -Uri $uri if ($null -eq $response) { break } $parsed = $response.Content | ConvertFrom-Json if ($null -ne $parsed) { $refs += @($parsed) } $uri = Get-QBuildMapToolsNextLink -Link $response.Headers['Link'] } return $refs } function Get-QBuildMapToolsRelease { param([string]$Version) $tag = Select-QBuildMapToolsTag -Refs @(Get-QBuildMapToolsTagRef) -Version $Version $response = Invoke-QBuildMapToolsGitHubGet -Uri "https://api.github.com/repos/qbikez/ps-publishmap/releases/tags/$($tag.Tag)" if ($null -eq $response) { throw "GitHub release '$($tag.Tag)' was not found." } return Select-QBuildMapToolsAsset -Release ($response.Content | ConvertFrom-Json) } function Install-QBuildMapTools { <# .SYNOPSIS Download a qbuild Map Tools release and install its VSIX. .PARAMETER Version Extension version to install, such as 0.1.1. Defaults to QBUILD_MAP_TOOLS_VERSION, then to the latest release. .PARAMETER Editor Editor CLI to install into: code, cursor, or code-insiders. Defaults to QBUILD_EDITOR, then to every detected editor. #> [CmdletBinding()] param( [string]$Version, [string]$Editor, [scriptblock]$GetRelease = { param($RequestedVersion) Get-QBuildMapToolsRelease -Version $RequestedVersion }, [scriptblock]$ResolveEditor = { param($Name) Resolve-QBuildMapToolsEditor -Editor $Name }, [scriptblock]$SaveAsset = { param($Url, $Destination) Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing }, [scriptblock]$InstallExtension = { param($Command, $VsixPath) & $Command --install-extension $VsixPath --force if ($LASTEXITCODE -ne 0) { throw "Installing '$VsixPath' with '$Command' failed." } } ) if ([string]::IsNullOrWhiteSpace($Version) -and -not [string]::IsNullOrWhiteSpace($env:QBUILD_MAP_TOOLS_VERSION)) { $Version = $env:QBUILD_MAP_TOOLS_VERSION } if ([string]::IsNullOrWhiteSpace($Editor) -and -not [string]::IsNullOrWhiteSpace($env:QBUILD_EDITOR)) { $Editor = $env:QBUILD_EDITOR } $destination = $null $previousProgress = $ProgressPreference try { $ProgressPreference = 'SilentlyContinue' $release = & $GetRelease $Version $editors = @(& $ResolveEditor $Editor) if ($editors.Count -eq 0 -or $null -eq $editors[0]) { throw "No editor CLI was found. Install the 'code', 'cursor', or 'code-insiders' command from the Command Palette, then run this script again." } $destination = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $release.AssetName Write-Host "Downloading qbuild Map Tools $($release.Version)..." & $SaveAsset $release.DownloadUrl $destination foreach ($item in $editors) { Write-Host "Installing $($release.AssetName) with $($item.Name)..." & $InstallExtension $item.Command $destination Write-Host "Installed qbuild Map Tools $($release.Version) with $($item.Name). Reload the editor to use it." -ForegroundColor Green } } finally { $ProgressPreference = $previousProgress if ($null -ne $destination -and (Test-Path -LiteralPath $destination)) { Remove-Item -LiteralPath $destination -Force } } } if ($MyInvocation.InvocationName -ne '.') { $installerFunctions = @( 'Get-QBuildMapToolsNextLink' 'Select-QBuildMapToolsTag' 'Select-QBuildMapToolsAsset' 'Get-QBuildMapToolsEditorPath' 'Resolve-QBuildMapToolsEditor' 'Invoke-QBuildMapToolsGitHubGet' 'Get-QBuildMapToolsTagRef' 'Get-QBuildMapToolsRelease' 'Install-QBuildMapTools' ) try { Install-QBuildMapTools } finally { if ([string]::IsNullOrWhiteSpace($PSCommandPath)) { foreach ($name in $installerFunctions) { Remove-Item -LiteralPath "Function:\$name" -ErrorAction SilentlyContinue } } } } |