core/public/Find-JaxTasks.ps1

function Find-JaxTasks {
    <#
    .SYNOPSIS
    Search task keys across environments.

    .DESCRIPTION
    Returns one object per (task, environment) match, sorted by task then
    environment. EnvPattern limits which environments are searched, with the
    same matching rules as Pattern. An environment that fails to load is
    skipped with a warning, so one broken environment never hides the others.

    Discovering every environment takes a while, so in a git repository the
    task names of all environments are kept in .jax/cache/task-index.json and
    reused until a .ps1/.psm1/.yml/.yaml file changes (Get-JaxTaskIndexFingerprint).
    Refresh rebuilds it; NoCache neither reads nor writes it.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $RepoRoot,
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary] $Config,
        [Parameter(Mandatory = $true)]
        [string] $Pattern,
        [string] $EnvPattern,
        [switch] $Refresh,
        [switch] $NoCache
    )

    $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters

    $cacheDir = Join-Path $RepoRoot '.jax/cache'
    $cachePath = Join-Path $cacheDir 'task-index.json'
    $fingerprint = if ($NoCache) { $null } else { Get-JaxTaskIndexFingerprint -RepoRoot $RepoRoot @commonParams }

    $index = $null
    if ($null -ne $fingerprint -and -not $Refresh -and (Test-Path -LiteralPath $cachePath -PathType Leaf)) {
        try {
            $cached = Get-Content -LiteralPath $cachePath -Raw | ConvertFrom-Json -AsHashtable
            if ($cached['fingerprint'] -eq $fingerprint) {
                $index = @($cached['environments'])
            }
        } catch {
            $index = $null
        }
    }

    if ($null -eq $index) {
        $envs = @(Get-JaxEnvironments -RepoRoot $RepoRoot -Config $Config @commonParams | Sort-Object -Property Name -Unique)
        # Without a reusable index, only the requested environments are worth discovering.
        if ($null -eq $fingerprint -and -not [string]::IsNullOrWhiteSpace($EnvPattern)) {
            $envs = @($envs | Where-Object { Test-JaxTaskPatternMatch -Name $_.Name -Pattern $EnvPattern })
        }
        Write-Host "Indexing tasks of $($envs.Count) environments..."
        $skipped = 0
        $index = @(foreach ($environment in $envs) {
                try {
                    $taskKeys = Get-JaxEnvironmentTaskKeys -RepoRoot $RepoRoot -Config $Config -Environment $environment @commonParams
                    @{ name = $environment.Name; tasks = @($taskKeys.Keys | Sort-Object -Unique) }
                } catch {
                    $skipped++
                    Write-Warning "Skipped environment '$($environment.Name)': $($_.Exception.Message)"
                }
            })
        # An incomplete index is not saved, so the next search retries the skipped environments.
        if ($null -ne $fingerprint -and $skipped -eq 0) {
            try {
                New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
                $ignoreFile = Join-Path $cacheDir '.gitignore'
                if (-not (Test-Path -LiteralPath $ignoreFile)) {
                    Set-Content -LiteralPath $ignoreFile -Value '*'
                }
                $temp = "$cachePath.$PID.tmp"
                @{ fingerprint = $fingerprint; environments = $index } | ConvertTo-Json -Depth 4 -Compress | Set-Content -LiteralPath $temp
                Move-Item -LiteralPath $temp -Destination $cachePath -Force
            } catch {
                Write-Warning "Could not save the task index: $($_.Exception.Message)"
            }
        }
    }

    $results = foreach ($entry in $index) {
        if (-not [string]::IsNullOrWhiteSpace($EnvPattern) -and -not (Test-JaxTaskPatternMatch -Name $entry['name'] -Pattern $EnvPattern)) {
            continue
        }
        foreach ($task in @($entry['tasks'])) {
            if (Test-JaxTaskPatternMatch -Name $task -Pattern $Pattern) {
                [pscustomobject]@{ Task = $task; Env = $entry['name'] }
            }
        }
    }

    return @($results | Sort-Object -Property Task, Env)
}