core/private/Install-JaxGalleryPackage.ps1

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

        Nothing is written straight into ~/.jax/module: a half-downloaded module
        directory is worse than an old one.
    #>

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

        [string] $Repository = 'PSGallery'
    )

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

    Write-Host (" Downloading Jax {0} from the {1}..." -f $Version, $Repository) -ForegroundColor Cyan
    $saved = $false
    try {
        if (Get-Command Save-PSResource -ErrorAction SilentlyContinue) {
            Save-PSResource -Name 'Jax' -Version $Version.ToString() -Repository $Repository `
                -Path $stagingParent -TrustRepository -ErrorAction Stop
            $saved = $true
        } elseif (Get-Command Save-Module -ErrorAction SilentlyContinue) {
            Save-Module -Name 'Jax' -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 ./Install-Jax.ps1.' -ForegroundColor DarkGray
        }
    } catch {
        Write-Host (" Download failed: {0}" -f $_.Exception.Message) -ForegroundColor Red
    }

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

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

function Install-JaxStagedPackage {
    <#
    .SYNOPSIS
        Install a downloaded Jax package into the install root.
    .DESCRIPTION
        Preferred path: run the package's own Install-Jax.ps1, so staging, backup,
        symlink rejection, manifest validation, and shell integration all come from
        the release being installed rather than from a second implementation here.

        It runs in a child pwsh because the installer removes and re-imports the
        Jax module as its last step, and doing that inside the `jax update` that is
        still executing would pull the running command's own module out from under
        it.

        Fallback: releases published before Install-Jax.ps1 shipped in the package
        get a plain staged directory swap - move the old install aside, copy the
        new one in, put the old one back if that fails - so `jax update` still
        works against them.
    #>

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

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

    $installer = Join-Path $StagedRoot 'Install-Jax.ps1'
    if (Test-Path -LiteralPath $installer -PathType Leaf) {
        $pwshPath = [Environment]::ProcessPath
        if ([string]::IsNullOrWhiteSpace($pwshPath)) { $pwshPath = 'pwsh' }
        & $pwshPath -NoLogo -NoProfile -File $installer -InstallRoot $InstallRoot
        if ($LASTEXITCODE -eq 0) { return $true }
        Write-Host (" The package installer exited with code {0}." -f $LASTEXITCODE) -ForegroundColor Red
        return $false
    }

    Write-Host ' This release predates the packaged installer - falling back to a direct swap.' -ForegroundColor DarkGray
    return Copy-JaxStagedPackage -StagedRoot $StagedRoot -InstallRoot $InstallRoot
}

function Copy-JaxStagedPackage {
    <#
    .SYNOPSIS
        Swap a staged package directory into the install root.
    .DESCRIPTION
        The old install is moved aside first and put back if the copy fails, so an
        interrupted update cannot leave the user with no Jax at all. ~/.jax/shell
        is refreshed too: the zsh and bash wrappers live there rather than in the
        module directory, and a wrapper that has drifted from the module it loads
        fails in ways 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 ('.jax-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-JaxShadowWarning {
    <#
    .SYNOPSIS
        Report Jax copies on PSModulePath that a bare Import-Module would pick over
        the one Jax actually runs, and offer to remove them.
    .DESCRIPTION
        Install-Jax.ps1 and `jax update` both target ~/.jax/module, which is not on
        PSModulePath. A leftover `Install-Module Jax` therefore keeps answering
        `Import-Module Jax` - including from scripts, plugins, 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 Jax 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 Jax` resolves to that copy, not the one your shell runs.' -ForegroundColor DarkGray

    if ($Check) { return }

    $remove = [bool]$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 {
            # Uninstall-PSResource cleans the PSResourceGet bookkeeping as well as
            # the directory; a plain delete leaves the install registered.
            if (Get-Command Uninstall-PSResource -ErrorAction SilentlyContinue) {
                Uninstall-PSResource -Name 'Jax' -Version $copy.Version.ToString() -SkipDependencyCheck -ErrorAction Stop
            } else {
                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
        }
    }
}