Private/Get-GraphCatalogPath.ps1

function Get-GraphCatalogPath {
    <#
    .SYNOPSIS
        Resolves the on-disk location of the GraphShell catalog files.
    .DESCRIPTION
        GraphShell does not maintain its own copy of the Microsoft Graph catalog. It reuses
        the files generated by scripts/sync-graph-command-index.ps1, the same source consumed
        by the web Explorer.

        Each file/folder is resolved independently, in this order of candidate roots:
          1. $env:GRAPHSHELL_CATALOG_PATH, when set (used by tests and advanced scenarios).
          2. The module's own Data/ folder, when it contains that file
             (populated by scripts/build-graphshell-module.ps1 for offline/packaged use).
          3. The repository's data/ folder, located by walking up from the module root
             (development scenario: the module lives inside the GraphShell repository).

        This means a packaged module can ship the compact index/permissions/renames files in
        Data/ (small, ~11.8 MB) while still lazy-loading graph-command-details/ shards from
        the repository when the packaged module does not include them.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    $moduleRoot = Split-Path -Parent $PSScriptRoot
    $candidateRoots = [System.Collections.Generic.List[string]]::new()

    if ($env:GRAPHSHELL_CATALOG_PATH) {
        $candidateRoots.Add($env:GRAPHSHELL_CATALOG_PATH)
    }

    $candidateRoots.Add((Join-Path $moduleRoot 'Data'))

    # Walk up from the module root looking for a sibling 'data' folder (repo dev mode).
    $probe = $moduleRoot
    for ($i = 0; $i -lt 5; $i++) {
        $probe = Split-Path -Parent $probe
        if (-not $probe) { break }
        $candidateRoots.Add((Join-Path $probe 'data'))
    }

    function Resolve-CatalogItem {
        param([string]$RelativeName)
        foreach ($root in $candidateRoots) {
            $candidate = Join-Path $root $RelativeName
            if ($root -and (Test-Path $candidate)) { return $candidate }
        }
        return $null
    }

    $indexPath = Resolve-CatalogItem -RelativeName 'graph-command-index.json'
    if (-not $indexPath) {
        throw "GraphShell could not locate the catalog (graph-command-index.json). Set `$env:GRAPHSHELL_CATALOG_PATH or run scripts/sync-graph-command-index.ps1."
    }

    [pscustomobject]@{
        Root                = Split-Path -Parent $indexPath
        IndexPath           = $indexPath
        PermissionsPath     = Resolve-CatalogItem -RelativeName 'graph-command-permissions.json'
        RenamesPath         = Resolve-CatalogItem -RelativeName 'graph-command-renames.json'
        DetailsPath         = Resolve-CatalogItem -RelativeName 'graph-command-details'
        SynonymsPath        = Resolve-CatalogItem -RelativeName 'graph-search-synonyms.json'
        # Lightweight, sharded reverse indexes (scripts/build-graphshell-lite-indexes.ps1) so
        # -Cmdlet/-Endpoint/-Module/-Permission do not need to load the full index above.
        ByCmdletPath        = Resolve-CatalogItem -RelativeName 'graph-command-by-cmdlet'
        AliasesPath         = Resolve-CatalogItem -RelativeName 'graph-command-aliases.json'
        ByEndpointPath      = Resolve-CatalogItem -RelativeName 'graph-command-by-endpoint'
        ByModulePath        = Resolve-CatalogItem -RelativeName 'graph-command-by-module'
        ModuleNamesPath     = Resolve-CatalogItem -RelativeName 'graph-command-by-module-names.json'
        PermissionsLitePath = Resolve-CatalogItem -RelativeName 'graph-command-permissions-lite.json'
    }
}