Private/Get-LatestClaudeCodeVersion.ps1

function Get-LatestClaudeCodeVersion {
    <#
    .SYNOPSIS
        Resolves the latest published @anthropic-ai/claude-code version from the npm registry.
    .DESCRIPTION
        Queries the npm registry HTTP endpoint directly (no Node.js dependency) and returns
        the latest version string, or $null on any failure (network down, parse error). The
        result is cached in a host file under the user config directory so repeated calls
        within the TTL avoid network round-trips.

        This never throws and never Write-Errors on transient failures — callers decide how
        to handle a $null result (typically: keep using the existing runtime).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter()]
        [int]$MaxAgeHours = 4,

        [Parameter()]
        [switch]$Force
    )

    $configDir = Join-Path $HOME '.dclaude'
    $cacheFile = Join-Path $configDir '.cc-latest-cache.json'

    # Try the cache first unless -Force was given.
    if (-not $Force -and (Test-Path -Path $cacheFile -PathType Leaf)) {
        try {
            $cache = Get-Content -Path $cacheFile -Raw | ConvertFrom-Json
            $hasVersion = $cache.PSObject.Properties['version'] -and $cache.version
            $hasTimestamp = $cache.PSObject.Properties['checkedAtUtc'] -and $cache.checkedAtUtc
            if ($hasVersion -and $hasTimestamp) {
                $checkedAt = [datetime]::Parse(
                    $cache.checkedAtUtc, $null, [System.Globalization.DateTimeStyles]::AssumeUniversal -bor
                    [System.Globalization.DateTimeStyles]::AdjustToUniversal)
                $age = [datetime]::UtcNow - $checkedAt
                if ($age.TotalHours -lt $MaxAgeHours) {
                    Write-Verbose "dclaude: using cached latest Claude Code version ($($cache.version), age $([int]$age.TotalMinutes)m)"
                    return [string]$cache.version
                }
            }
        }
        catch {
            # Missing/corrupt cache is just a cache miss — fall through to the network query.
            Write-Verbose "dclaude: latest-version cache unreadable, querying registry: $_"
        }
    }

    # The scope separator '/' must be URL-encoded as %2F in the registry path.
    $url = 'https://registry.npmjs.org/@anthropic-ai%2Fclaude-code/latest'
    try {
        $response = Invoke-RestMethod -Uri $url -ErrorAction Stop
        if (-not ($response.PSObject.Properties['version'] -and $response.version)) {
            Write-Verbose 'dclaude: registry response had no version field'
            return $null
        }
        $version = [string]$response.version
    }
    catch {
        Write-Verbose "dclaude: failed to query npm registry for latest Claude Code version: $_"
        return $null
    }

    # Best-effort cache write — failure here must not fail the call. -ErrorAction Stop
    # promotes non-terminating errors (e.g. $configDir exists as a non-directory) to the
    # catch so nothing leaks to the caller's error stream.
    try {
        if (-not (Test-Path -Path $configDir)) {
            New-Item -ItemType Directory -Path $configDir -Force -ErrorAction Stop | Out-Null
        }
        [PSCustomObject]@{
            version      = $version
            checkedAtUtc = [datetime]::UtcNow.ToString('o')
        } | ConvertTo-Json | Set-Content -Path $cacheFile -Encoding UTF8 -ErrorAction Stop
    }
    catch {
        Write-Verbose "dclaude: could not write latest-version cache: $_"
    }

    return $version
}