shims/gh.ps1

#Requires -Version 7
<#
    Identity guard for the GitHub CLI.

    WHY THIS EXISTS

    `gh` reads its account from a configuration directory named by
    GH_CONFIG_DIR. Without that variable it falls back to the machine-wide
    config -- that is, to whichever account was logged in last. `work` sets the
    variable, but `work` is a PowerShell command: from git-bash, an npm script
    or an agent's shell it was never set.

    That is the failure this whole module was built around, and until now it was
    handled by discipline: "never run gh from bash". A rule the tool can enforce
    itself has no business living in somebody's memory.

    CONTRACT -- IT CORRECTS BEFORE IT REFUSES

    The supabase guard can only refuse: nobody can guess which database was
    meant. Here the right answer is known, because the folder gives it. So:

      GH_CONFIG_DIR unset, the context has its config -> set it, silently
      GH_CONFIG_DIR unset, a `gh auth ...` command -> set it, and say so
      GH_CONFIG_DIR unset, no config yet, a WRITE -> refuse, name the fix
      GH_CONFIG_DIR set and matching -> nothing to do
      GH_CONFIG_DIR set on ANOTHER context -> refuse writes

    A read is never refused, only flagged. Refusing reads would cost more than
    it protects, and a blocked user reaches for the raw binary -- with no guard
    at all.

    The variable is set on THIS process, which exists only to launch `gh`. The
    caller's session is never modified.

    Delegates unchanged whenever it is not certain: no context owning the
    folder, a missing or broken module, any unexpected error.

    Exit code is the real CLI's, except on refusal, which exits 1.

    Console messages stay pure ASCII: an em-dash renders as '-' in git-bash and
    worse elsewhere, and a refusal is the one output that must read correctly on
    a machine we know nothing about.

    No param() block on purpose: [CmdletBinding()] would swallow arguments like
    -debug or -verbose as its own parameters instead of forwarding them.
#>


Set-StrictMode -Off
$ErrorActionPreference = 'Stop'

$ShimDir = $PSScriptRoot
$Arguments = @($args)

# Meme garde-fou anti-boucle que shims/supabase.ps1, et meme raison : depuis que
# PATH designe une jonction vers le module, un meme dossier porte deux noms. Si
# les deux se retrouvent dans PATH, chaque shim ecarte le sien, trouve l'autre,
# et l'appelle sans fin. Un compteur ne ment pas, la ou un chemin ment
# volontiers : jonctions, casse, noms 8.3, lecteurs subst, chemins UNC.
#
# Il INTERROMPT la boucle, il ne saute jamais le controle : poser la variable a
# la main ne peut donc qu'echouer plus tot, jamais obtenir un passe-droit.
$Profondeur = 0
if ($env:DEVCTX_SHIM_DEPTH) { $Profondeur = [int]$env:DEVCTX_SHIM_DEPTH }
if ($Profondeur -ge 3) {
    [Console]::Error.WriteLine('')
    [Console]::Error.WriteLine(' DevContext: shim loop detected -- a shim resolved to another shim.')
    [Console]::Error.WriteLine(' Two DevContext shim directories are probably both in PATH.')
    [Console]::Error.WriteLine(' Fix: pwsh -File installer-shims.ps1 -Verifier')
    [Console]::Error.WriteLine('')
    exit 1
}
$env:DEVCTX_SHIM_DEPTH = $Profondeur + 1

# --- delegation -------------------------------------------------------------

function Resolve-RealExe {
    # Deliberately duplicated from the module rather than imported: this must
    # still work when the module is missing or broken, which is exactly when
    # delegation matters most.
    $here = $ShimDir.TrimEnd('\', '/')
    Get-Command gh -CommandType Application -All -ErrorAction SilentlyContinue |
        Where-Object { (Split-Path $_.Source -Parent).TrimEnd('\', '/') -ne $here } |
        Select-Object -First 1 -ExpandProperty Source
}

# La langue du shim. Il doit pouvoir refuser meme si le module est absent, donc
# il source le fichier de langue directement et se rabat sur une phrase anglaise
# codee en dur si meme cela echoue. Un refus muet serait pire qu'un refus mal
# traduit.
$Traduit = $false
try {
    . (Join-Path $PSScriptRoot '..' 'src' 'Langue.ps1')
    Set-CtxLangue | Out-Null
    $Traduit = $true
}
catch { $Traduit = $false }

function Dire {
    param([string]$Cle, [string]$Secours, [object[]]$Arguments)
    if (-not $Traduit) {
        if ($Arguments) { return ($Secours -f $Arguments) }
        return $Secours
    }
    if ($Arguments) { return (T $Cle @Arguments) }
    T $Cle
}

function Invoke-Real {
    $exe = Resolve-RealExe
    if (-not $exe) {
        Write-Error (Dire 'garde.introuvable' '{0} not found in PATH (outside the shims).' @('gh'))
        exit 127
    }
    & $exe @Arguments
    exit $LASTEXITCODE
}

# --- decide -----------------------------------------------------------------
#
# La regle vit dans le module (src/Gh.ps1), jamais ici. Lecon du 16 aout 2026 :
# une regle ecrite dans un shim n'existe que pour les appelants qui traversent
# ce shim -- voir CHANGELOG 1.3.5.

$module   = $null
$verdict  = $null

try {
    $module  = Import-Module (Join-Path $PSScriptRoot '..' 'DevContext.psd1') -Force -PassThru -ErrorAction Stop
    $verdict = & $module {
        param($a, $p) Resolve-CtxGhVerdict -Arguments $a -Path $p
    } $Arguments $PWD.Path
}
catch {
    Invoke-Real
}

if (-not $verdict) { Invoke-Real }

# stderr, jamais stdout : `gh api`, `gh pr view --json` et consorts sont lus par
# des scripts, et un mot de plus sur leur sortie casse un jq en aval.
if ($verdict.Avertissement) { [Console]::Error.WriteLine(" $($verdict.Avertissement)") }

if ($verdict.Allowed) {
    # Sur CE processus, qui n'existe que pour lancer gh. L'enfant en herite ;
    # la session de l'appelant n'est pas touchee.
    if ($verdict.Redirection) { $env:GH_CONFIG_DIR = $verdict.Redirection }
    Invoke-Real
}

# --- refuse -----------------------------------------------------------------
#
# Hors du try, deliberement : une levee pendant l'affichage retomberait sinon
# dans Invoke-Real, et transformerait un refus en execution.

& $module { param($v) Write-CtxGhRefus -Verdict $v } $verdict

exit 1