core/private/Get-JaxInstallInfo.ps1

function Get-JaxInstallInfo {
    <#
    .SYNOPSIS
        Describe how the running copy of Jax got onto this machine.
    .DESCRIPTION
        Jax can be loaded three ways, and the right way to update it differs for
        each one:

          Repo - run straight from a source checkout. Updating means `git pull`.
          Manual - installed into ~/.jax/module by Install-Jax.ps1 from a
                    checkout. Update-PSResource does not touch this copy at all,
                    because PSResourceGet never installed it.
          Gallery - the published package, either installed into ~/.jax/module by
                    `jax update` or sitting under a PSModulePath entry.

        Telling a manual install to run `Update-PSResource Jax` is the failure this
        exists to prevent: it either errors or updates a different copy that the
        `jx` shim never loads, because the shim resolves ~/.jax/module through
        shell/Jax.ShellLauncher.ps1 rather than through PSModulePath.

        Origin comes from INSTALLATION.json: Build-JaxPackage stamps 'Gallery' and
        Install-Jax.ps1 copies that forward. Records written before Origin existed
        report Manual, which is what they are.
    .PARAMETER RuntimeRoot
        The Jax runtime being described - the directory holding Jax.psd1.
    .PARAMETER IncludeGalleryCopies
        Also enumerate Jax modules on PSModulePath and work out whether one shadows
        the loaded copy for a bare `Import-Module Jax`. Off by default because
        enumerating PSModulePath is far too slow for the per-run update notice.
    .EXAMPLE
        Get-JaxInstallInfo -RuntimeRoot $PSScriptRoot -IncludeGalleryCopies
    #>

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

        [string] $InstallRoot = (Join-Path $HOME '.jax/module'),

        [switch] $IncludeGalleryCopies
    )

    $resolvedRoot = try { [IO.Path]::GetFullPath($RuntimeRoot) } catch { $RuntimeRoot }
    $resolvedInstall = try { [IO.Path]::GetFullPath($InstallRoot) } catch { $InstallRoot }

    $version = $null
    $manifestPath = Join-Path $resolvedRoot 'Jax.psd1'
    if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
        try {
            $parsed = $null
            $text = [string](Import-PowerShellDataFile -LiteralPath $manifestPath).ModuleVersion
            if ([version]::TryParse($text, [ref] $parsed)) { $version = $parsed }
        } catch {
            # An unreadable manifest is an unknown version, not a failed command.
        }
    }

    $record = Read-JaxJsonFile -Path (Join-Path $resolvedRoot 'INSTALLATION.json')
    $origin = [string](Get-JaxJsonProperty -Object $record -Name 'Origin')

    $flavour = if ($resolvedRoot -eq $resolvedInstall) {
        if ($origin -in @('Gallery', 'Manual')) { $origin } else { 'Manual' }
    } elseif (Test-JaxPathUnderModulePath -Path $resolvedRoot) {
        'Gallery'
    } else {
        'Repo'
    }

    $galleryCopies = @()
    $shadowedBy = $null
    if ($IncludeGalleryCopies) {
        try {
            $galleryCopies = @(
                Get-Module -ListAvailable -Name 'Jax' -ErrorAction SilentlyContinue |
                    Where-Object { [IO.Path]::GetFullPath($_.ModuleBase) -ne $resolvedRoot } |
                    Sort-Object Version -Descending |
                    ForEach-Object { [pscustomobject]@{ Version = $_.Version; Path = $_.ModuleBase } }
            )
        } catch {
            $galleryCopies = @()
        }
        $shadowedBy = @($galleryCopies | Where-Object { $null -eq $version -or $_.Version -ne $version }) |
            Select-Object -First 1
    }

    [pscustomobject]@{
        Flavour        = $flavour
        RuntimeRoot    = $resolvedRoot
        InstallRoot    = $resolvedInstall
        Version        = $version
        SourceCommit   = [string](Get-JaxJsonProperty -Object $record -Name 'SourceCommit')
        InstalledAtUtc = ConvertTo-JaxUtcDate -Value (Get-JaxJsonProperty -Object $record -Name 'InstalledAtUtc')
        GalleryCopies  = $galleryCopies
        ShadowedBy     = $shadowedBy
        UpdateCommand  = if ($flavour -eq 'Repo') { 'git pull, then ./Install-Jax.ps1' } else { 'jax update' }
    }
}

function Test-JaxPathUnderModulePath {
    <#
    .SYNOPSIS
        True when Path sits inside one of the PSModulePath roots.
    .DESCRIPTION
        Distinguishes an Install-Module copy from a checkout. Compared as
        normalised path prefixes so a trailing separator or a relative
        PSModulePath entry does not produce a false negative.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    $target = try { [IO.Path]::GetFullPath($Path) } catch { return $false }
    $separator = [IO.Path]::DirectorySeparatorChar

    foreach ($entry in @([string]$env:PSModulePath -split [IO.Path]::PathSeparator)) {
        if ([string]::IsNullOrWhiteSpace($entry)) { continue }
        $root = try { [IO.Path]::GetFullPath($entry) } catch { continue }
        $prefix = $root.TrimEnd($separator, [IO.Path]::AltDirectorySeparatorChar) + $separator
        if ($target.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { return $true }
    }

    return $false
}