private/Install-WtwGalleryPackage.ps1

function Save-WtwGalleryPackage {
    <#
    .SYNOPSIS
        Download a wtw release from the PowerShell Gallery into a temp directory.
    .DESCRIPTION
        Returns the directory that actually contains wtw.psm1, or $null when the
        download fails. Save-PSResource and Save-Module lay the package out
        differently (flat versus a version subdirectory), so the module root is
        located by looking for the manifest rather than by assuming a shape.

        Nothing is downloaded straight into ~/.wtw/module: a half-written module
        directory is worse than an old one, so the package is staged first and
        only swapped in once it is complete.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [version] $Version,

        [string] $Repository = 'PSGallery'
    )

    $stagingParent = Join-Path ([IO.Path]::GetTempPath()) ('wtw-update-{0}' -f [guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $stagingParent -Force | Out-Null

    Write-Host (" Downloading wtw {0} from the {1}..." -f $Version, $Repository) -ForegroundColor Cyan
    $saved = $false
    try {
        if (Get-Command Save-PSResource -ErrorAction SilentlyContinue) {
            Save-PSResource -Name 'wtw' -Version $Version.ToString() -Repository $Repository `
                -Path $stagingParent -TrustRepository -ErrorAction Stop
            $saved = $true
        } elseif (Get-Command Save-Module -ErrorAction SilentlyContinue) {
            Save-Module -Name 'wtw' -RequiredVersion $Version.ToString() -Repository $Repository `
                -Path $stagingParent -Force -ErrorAction Stop
            $saved = $true
        } else {
            Write-Host ' Neither Save-PSResource nor Save-Module is available.' -ForegroundColor Red
            Write-Host ' Install Microsoft.PowerShell.PSResourceGet, or update from a checkout with `wtw install`.' -ForegroundColor DarkGray
        }
    } catch {
        Write-Host (" Download failed: {0}" -f $_.Exception.Message) -ForegroundColor Red
    }

    if ($saved) {
        $manifest = Get-ChildItem -LiteralPath $stagingParent -Filter 'wtw.psm1' -Recurse -File -ErrorAction SilentlyContinue |
            Select-Object -First 1
        if ($manifest) {
            return $manifest.Directory.FullName
        }
        Write-Host ' The downloaded package did not contain wtw.psm1.' -ForegroundColor Red
    }

    Remove-Item -LiteralPath $stagingParent -Recurse -Force -ErrorAction SilentlyContinue
    return $null
}

function Install-WtwStagedModule {
    <#
    .SYNOPSIS
        Swap a staged module directory into the install root.
    .DESCRIPTION
        Moves the existing install aside before putting the new one in place, and
        puts it back if the move fails, so an interrupted update cannot leave the
        user with no wtw at all.

        ~/.wtw/shell is refreshed from the staged copy too. The zsh and bash
        wrappers live there, not in the module directory, and a wrapper that has
        drifted from the module it loads produces failures that look nothing like
        "the update half-finished".
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $StagedRoot,

        [Parameter(Mandatory)]
        [string] $InstallRoot
    )

    $parent = Split-Path -Parent $InstallRoot
    if (-not (Test-Path -LiteralPath $parent -PathType Container)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }

    $backup = Join-Path $parent ('.wtw-backup-{0}' -f [guid]::NewGuid().ToString('N'))
    $hadPrevious = Test-Path -LiteralPath $InstallRoot

    try {
        if ($hadPrevious) {
            Move-Item -LiteralPath $InstallRoot -Destination $backup -Force -ErrorAction Stop
        }
        Copy-Item -LiteralPath $StagedRoot -Destination $InstallRoot -Recurse -Force -ErrorAction Stop

        $shellSource = Join-Path $InstallRoot 'shell'
        if (Test-Path -LiteralPath $shellSource -PathType Container) {
            $shellDest = Join-Path $parent 'shell'
            if (Test-Path -LiteralPath $shellDest) {
                Remove-Item -LiteralPath $shellDest -Recurse -Force -ErrorAction SilentlyContinue
            }
            Copy-Item -LiteralPath $shellSource -Destination $shellDest -Recurse -Force -ErrorAction SilentlyContinue
        }

        if ($hadPrevious) {
            Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction SilentlyContinue
        }
        return $true
    } catch {
        Write-Host (" Install failed: {0}" -f $_.Exception.Message) -ForegroundColor Red
        if ($hadPrevious -and (Test-Path -LiteralPath $backup)) {
            if (Test-Path -LiteralPath $InstallRoot) {
                Remove-Item -LiteralPath $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
            }
            Move-Item -LiteralPath $backup -Destination $InstallRoot -Force -ErrorAction SilentlyContinue
            Write-Host ' Previous install restored.' -ForegroundColor Yellow
        }
        return $false
    }
}

function Write-WtwShadowWarning {
    <#
    .SYNOPSIS
        Report wtw copies on PSModulePath that a bare Import-Module would pick
        over the one wtw actually loads, and offer to remove them.
    .DESCRIPTION
        `wtw install` and `wtw update` both target ~/.wtw/module, which is not on
        PSModulePath. A leftover `Install-Module wtw` therefore keeps answering
        `Import-Module wtw` — including from scripts and other modules — with a
        version nobody is maintaining. Silent until such a copy exists and its
        version differs.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Info,

        [switch] $Yes,

        [switch] $Check
    )

    if (-not $Info.ShadowedBy) { return }

    Write-Host ''
    Write-Host ' Another wtw is installed on PSModulePath:' -ForegroundColor Yellow
    foreach ($copy in $Info.GalleryCopies) {
        Write-Host (" {0} {1}" -f $copy.Version, $copy.Path) -ForegroundColor DarkGray
    }
    Write-Host ' `Import-Module wtw` resolves to that copy, not the one your shell loads.' -ForegroundColor DarkGray

    if ($Check) { return }

    $remove = $Yes
    if (-not $remove) {
        $remove = (Read-Host ' Remove the PSModulePath copies? [y/N]') -in @('y', 'Y', 'yes')
    }
    if (-not $remove) { return }

    foreach ($copy in $Info.GalleryCopies) {
        try {
            Remove-Item -LiteralPath $copy.Path -Recurse -Force -ErrorAction Stop
            Write-Host (" removed {0}" -f $copy.Path) -ForegroundColor Green
        } catch {
            Write-Host (" could not remove {0}: {1}" -f $copy.Path, $_.Exception.Message) -ForegroundColor Red
        }
    }
}