quinjet 0.0.3

A fast, live, keyboard-first Git source-control interface for the terminal
# Install Quinjet from GitHub Releases.
[CmdletBinding()]
param(
    [Parameter()]
    [string] $Version,

    [Parameter()]
    [string] $BinDir,

    [Parameter()]
    [switch] $NoModifyPath
)

Set-StrictMode -Version 3.0
$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol =
    [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

$Repository = "pulkitxm/quinjet"
$ReleasesUrl = "https://github.com/$Repository/releases"

function Write-Info {
    param([string] $Message)
    Write-Output "info: $Message"
}

function Get-BooleanEnvironmentValue {
    param(
        [string] $Name,
        [bool] $DefaultValue
    )

    $value = [Environment]::GetEnvironmentVariable($Name)
    if ([string]::IsNullOrWhiteSpace($value)) {
        return $DefaultValue
    }

    switch ($value.ToLowerInvariant()) {
        { $_ -in @("0", "false", "no") } { return $false }
        { $_ -in @("1", "true", "yes") } { return $true }
        default { throw "$Name must be 0 or 1" }
    }
}

function Test-PathEntry {
    param(
        [string] $PathValue,
        [string] $Entry
    )

    if ([string]::IsNullOrWhiteSpace($PathValue)) {
        return $false
    }

    $normalizedEntry = $Entry.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
    foreach ($candidate in $PathValue.Split([IO.Path]::PathSeparator)) {
        $normalizedCandidate = $candidate.Trim().TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
        if ($normalizedCandidate.Equals($normalizedEntry, [StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }
    return $false
}

function Invoke-Download {
    param(
        [string] $Uri,
        [string] $OutFile
    )

    try {
        Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing
    }
    catch {
        throw "failed to download ${Uri}: $($_.Exception.Message)"
    }
}

function Install-Quinjet {
    param(
        [string] $RequestedVersion,
        [string] $RequestedBinDir,
        [switch] $SkipPathUpdate
    )

    if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
        throw "install.ps1 only supports Windows; use install.sh on Linux or macOS"
    }

    if ([string]::IsNullOrWhiteSpace($RequestedVersion)) {
        $RequestedVersion = if ([string]::IsNullOrWhiteSpace($env:QUINJET_VERSION)) {
            "latest"
        }
        else {
            $env:QUINJET_VERSION
        }
    }

    if ([string]::IsNullOrWhiteSpace($RequestedBinDir)) {
        if (-not [string]::IsNullOrWhiteSpace($env:QUINJET_INSTALL_DIR)) {
            $RequestedBinDir = $env:QUINJET_INSTALL_DIR
        }
        elseif (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
            $RequestedBinDir = Join-Path $env:LOCALAPPDATA "Programs\Quinjet\bin"
        }
        elseif (-not [string]::IsNullOrWhiteSpace($HOME)) {
            $RequestedBinDir = Join-Path $HOME ".local\bin"
        }
        else {
            throw "could not determine an installation directory; set QUINJET_INSTALL_DIR"
        }
    }

    $skipPathUpdate = $SkipPathUpdate.IsPresent -or
        (Get-BooleanEnvironmentValue -Name "QUINJET_NO_MODIFY_PATH" -DefaultValue $false)

    $architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
    switch ($architecture) {
        "X64" { $asset = "quinjet-windows-x86_64.exe" }
        "Arm64" {
            $asset = "quinjet-windows-x86_64.exe"
            Write-Warning "a native Windows ARM64 build is not available; installing the x64 build"
        }
        default { throw "Quinjet does not publish a Windows release for architecture '$architecture'" }
    }

    if ($RequestedVersion -eq "latest") {
        $releaseUrl = "$ReleasesUrl/latest/download"
        $versionLabel = "latest"
    }
    else {
        $releaseTag = if ($RequestedVersion.StartsWith("v")) { $RequestedVersion } else { "v$RequestedVersion" }
        if ($releaseTag -notmatch '^v[0-9][0-9A-Za-z._+-]*$') {
            throw "invalid release version: $RequestedVersion"
        }
        $releaseUrl = "$ReleasesUrl/download/$releaseTag"
        $versionLabel = $releaseTag
    }

    $tempDir = Join-Path ([IO.Path]::GetTempPath()) ("quinjet-install-" + [Guid]::NewGuid().ToString("N"))
    $downloadPath = Join-Path $tempDir $asset
    $checksumsPath = Join-Path $tempDir "SHA256SUMS"
    $stagedBinary = $null

    New-Item -ItemType Directory -Path $tempDir | Out-Null
    try {
        Write-Info "detected Windows $architecture"
        Write-Info "downloading Quinjet $versionLabel"
        Invoke-Download -Uri "$releaseUrl/SHA256SUMS" -OutFile $checksumsPath
        Invoke-Download -Uri "$releaseUrl/$asset" -OutFile $downloadPath

        $escapedAsset = [Regex]::Escape($asset)
        $checksumPattern = "^(?<hash>[0-9A-Fa-f]{64})\s+\*?(?:dist/)?${escapedAsset}$"
        $checksumMatch = $null
        foreach ($line in Get-Content -LiteralPath $checksumsPath) {
            $match = [Regex]::Match($line.Trim(), $checksumPattern)
            if ($match.Success) {
                $checksumMatch = $match
                break
            }
        }
        if ($null -eq $checksumMatch) {
            throw "the release checksum for $asset is missing or invalid"
        }

        $expectedChecksum = $checksumMatch.Groups["hash"].Value
        $actualChecksum = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash
        if (-not $expectedChecksum.Equals($actualChecksum, [StringComparison]::OrdinalIgnoreCase)) {
            throw "checksum verification failed for $asset"
        }
        Write-Info "verified SHA-256 checksum"

        New-Item -ItemType Directory -Force -Path $RequestedBinDir | Out-Null
        $destination = Join-Path $RequestedBinDir "quinjet.exe"
        $stagedBinary = Join-Path $RequestedBinDir (".quinjet-install-" + [Guid]::NewGuid().ToString("N") + ".exe")
        Copy-Item -LiteralPath $downloadPath -Destination $stagedBinary
        Move-Item -LiteralPath $stagedBinary -Destination $destination -Force
        $stagedBinary = $null

        Write-Output "`nQuinjet was installed to $destination"

        $processPath = [Environment]::GetEnvironmentVariable("Path", "Process")
        if (-not (Test-PathEntry -PathValue $processPath -Entry $RequestedBinDir)) {
            if ($skipPathUpdate) {
                Write-Warning "$RequestedBinDir is not on PATH"
                Write-Output "Add it before using Quinjet:"
                Write-Output "  `$env:Path = `"$RequestedBinDir;`$env:Path`""
            }
            else {
                $pathUpdateFailed = $false
                $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
                if (-not (Test-PathEntry -PathValue $userPath -Entry $RequestedBinDir)) {
                    $updatedPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
                        $RequestedBinDir
                    }
                    else {
                        "$RequestedBinDir$([IO.Path]::PathSeparator)$userPath"
                    }
                    try {
                        [Environment]::SetEnvironmentVariable("Path", $updatedPath, "User")
                        Write-Info "added $RequestedBinDir to your user PATH"
                    }
                    catch {
                        $pathUpdateFailed = $true
                        Write-Warning "could not update your user PATH: $($_.Exception.Message)"
                    }
                }
                if ($pathUpdateFailed) {
                    Write-Output "Add it before using Quinjet:"
                    Write-Output "  `$env:Path = `"$RequestedBinDir;`$env:Path`""
                }
                else {
                    Write-Warning "restart your terminal before running quinjet"
                }
            }
        }

        if ($null -eq (Get-Command git -ErrorAction SilentlyContinue)) {
            Write-Warning "Git is required at runtime but was not found on PATH"
            Write-Warning "install Git from https://git-scm.com/download/win"
        }
    }
    finally {
        if ($null -ne $stagedBinary) {
            Remove-Item -LiteralPath $stagedBinary -Force -ErrorAction SilentlyContinue
        }
        Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
    }
}

Install-Quinjet -RequestedVersion $Version -RequestedBinDir $BinDir -SkipPathUpdate:$NoModifyPath