Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/pull.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: install.pull

on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
pull_install:
runs-on: windows-latest
defaults:
run:
shell: pwsh
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Context
run: |
$PSVersionTable
Get-Module -ListAvailable Pester | Sort-Object Version -Descending | Select-Object -First 1 | Format-List Name,Version,Path

- name: Test
env:
RUN_DESTRUCTIVE_TESTS: '1'
run: |
Import-Module Pester -MinimumVersion '5.0.0' -Force
New-Item -ItemType Directory -Force -Path TestResults | Out-Null

$config = [PesterConfiguration]::Default
$config.Run.Path = @('tests') # or '.' if your tests stay at repo root
$config.Output.Verbosity = 'Normal'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'TestResults/Pester-safe.xml'
$config.TestResult.OutputFormat = 'NUnitXml'

Invoke-Pester -Configuration $config

- name: Publish Results
if: always()
uses: actions/upload-artifact@v4
with:
name: pester-safe-results
path: TestResults/Pester-safe.xml
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ Usage:
...
```

## Tests

### Windows

We use [Pester](https://pester.dev/) to test the installation script. We develop
using Azure VM running Windows where there is an older version of Pester baked
in to the image (v3.4).

To ensure tests use Pester v5, configure your `$PROFILE`:

```powershell
# Clean out legacy module roots (prevents Pester 3.4 autoload)
$paths = $env:PSModulePath -split ';' | Where-Object {
$_ -notlike '*WindowsPowerShell*' -and $_ -notlike '*v1.0*'
}
$env:PSModulePath = ($paths -join ';')

# Force Pester 5
Remove-Module Pester -ErrorAction SilentlyContinue
Import-Module "$HOME\Documents\PowerShell\Modules\Pester\5.7.1\Pester.psd1" -Force
```

To run the tests use:

```powershell
Invoke-Pester -CI
```

## Requirements

- **macOS/Linux:** `curl`, `bash`
Expand Down
111 changes: 94 additions & 17 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<#
Usage:
.\install.ps1 → Install the Pair CLI
.\install.ps1 -Uninstall → Uninstall the Pair CLI
.\install.ps1 → Install the PairSpaces CLI
.\install.ps1 -Uninstall → Uninstall the PairSpaces CLI
#>

$ErrorActionPreference = "Stop"
Expand All @@ -14,7 +14,7 @@ $name = "pair"
$binary = "$name.exe"
$envName = "latest"
$baseUrl = "https://downloads.pairspaces.com/$envName"
$installDir = "$env:USERPROFILE\AppData\Local\$name"
$installDir = Join-Path $env:LOCALAPPDATA $name
$destBin = "$installDir\$binary"

# =============================================================================
Expand Down Expand Up @@ -85,14 +85,41 @@ function Make-Executable {
}

function Ensure-InPath {
$currentPath = [System.Environment]::GetEnvironmentVariable("Path", "User")
if (-not ($currentPath.Split(';') -contains $installDir)) {
# Open HKCU:\Environment and get the raw, non-expanded Path
$reg = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", $true)
if (-not $reg) { Show-Error "Could not open HKCU:\Environment for write." }

$rawPath = $reg.GetValue(
"Path", "",
[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames
)

$segments = @()
if ($rawPath) {
$segments = $rawPath -split ';' | Where-Object { $_ -and $_.Trim() -ne '' }
}

$expandedInstall = [Environment]::ExpandEnvironmentVariables($installDir).TrimEnd('\')
# Avoid duplicates by comparing expanded values of each segment
$alreadyPresent = $false
foreach ($seg in $segments) {
$expandedSeg = [Environment]::ExpandEnvironmentVariables($seg).TrimEnd('\')
if ([string]::Compare($expandedSeg, $expandedInstall, $true) -eq 0) {
$alreadyPresent = $true
break
}
}

if (-not $alreadyPresent) {
Show-Title "Adding to PATH" $installDir
$currentPath = $currentPath.TrimEnd(';')
$newPath = "$currentPath;$installDir"
[System.Environment]::SetEnvironmentVariable("Path", $newPath, "User")
$newRaw = ($segments + $installDir) -join ';'
# Write back as REG_EXPAND_SZ to preserve any %VARS% that may be present
$reg.SetValue("Path", $newRaw, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Refresh-Environment
Write-Host (" You may need to restart your terminal to use '${binary}'") -ForegroundColor Yellow
}

$reg.Close()
}

# =============================================================================
Expand All @@ -114,13 +141,35 @@ function Uninstall-App {
Info "Removed directory $installDir"
}

# Remove from PATH
$currentPath = [System.Environment]::GetEnvironmentVariable("Path", "User")
if ($currentPath.Split(';') -contains $installDir) {
$newPath = ($currentPath.Split(';') | Where-Object { $_ -ne $installDir }) -join ';'
[System.Environment]::SetEnvironmentVariable("Path", $newPath, "User")
Info "Removed $installDir from user PATH"
Write-Host (" You may need to restart your shell for changes to take effect.") -ForegroundColor Yellow
# Remove from PATH (preserves REG_EXPAND_SZ and respects %VARS%)
try {
$reg = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", $true)
if ($reg) {
$rawPath = $reg.GetValue("Path", "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)

if ($rawPath) {
$expandedInstall = [Environment]::ExpandEnvironmentVariables($installDir).TrimEnd('\')

$newSegs = @()
foreach ($seg in ($rawPath -split ';')) {
if (-not $seg) { continue }
$expandedSeg = [Environment]::ExpandEnvironmentVariables($seg).TrimEnd('\')
if ([string]::Compare($expandedSeg, $expandedInstall, $true) -ne 0) {
$newSegs += $seg
}
}

$reg.SetValue("Path", ($newSegs -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString)
Info "Removed $installDir from user PATH"
Refresh-Environment
Write-Host (" You may need to restart your shell for changes to take effect.") -ForegroundColor Yellow
}
} else {
Info "Could not open HKCU:\Environment to remove from PATH"
}
$reg.Close()
} catch {
Info "Failed to update PATH during uninstall: $($_.Exception.Message)"
}

# Remove directory
Expand All @@ -131,7 +180,33 @@ function Uninstall-App {
}

Show-Title "Uninstallation Complete"
exit 0
}

# =============================================================================
# Refresh Environment
# =============================================================================

function Refresh-Environment {
# Broadcast WM_SETTINGCHANGE "Environment" so new processes see updates immediately
$cs = @"
using System;
using System.Runtime.InteropServices;
public static class NativeMethods {
public const int HWND_BROADCAST = 0xffff;
public const int WM_SETTINGCHANGE = 0x1A;
public const int SMTO_ABORTIFHUNG = 0x0002;

[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
IntPtr hWnd, int Msg, IntPtr wParam, string lParam,
int fuFlags, int uTimeout, out IntPtr lpdwResult);
}
"@
Add-Type -TypeDefinition $cs -ErrorAction SilentlyContinue | Out-Null
[IntPtr]$out = [IntPtr]::Zero
[void][NativeMethods]::SendMessageTimeout(
[IntPtr]0xffff, 0x1A, [IntPtr]::Zero, "Environment",
0x0002, 5000, [ref]$out)
}

# =============================================================================
Expand Down Expand Up @@ -167,4 +242,6 @@ function Main {
Write-Host (" Restart your shell and run '${binary} help' to get started.") -ForegroundColor Green
}

Main @args
if ($MyInvocation.InvocationName -ne '.') {
Main @args
}
Loading