Payload/scripts/harness/lib/RepoRuntime.psm1

#Requires -Version 5.1
Set-StrictMode -Version Latest

function Test-RepoGitVersionSupported {
    param([Parameter(Mandatory = $true)][string]$GitExe)
    $version = (& $GitExe --version 2>$null) -replace '^git version\s+', '' -replace '\.windows.*$', ''
    if (-not $version) { return $false }
    $parts = $version.Split('.')
    if ($parts.Count -lt 2) { return $false }
    $major = [int]$parts[0]
    $minor = [int]$parts[1]
    return ($major -gt 2) -or ($major -eq 2 -and $minor -ge 32)
}

function Test-RepoNodeVersionSupported {
    param([Parameter(Mandatory = $true)][string]$NodeExe)
    & $NodeExe -e "const [M]=process.versions.node.split('.').map(Number); process.exit(M>=24?0:1)" 2>$null | Out-Null
    return $LASTEXITCODE -eq 0
}

function Resolve-RepoPortableGitExe {
    if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
        return $null
    }
    $path = Join-Path $env:LOCALAPPDATA 'Programs\PortableGit\cmd\git.exe'
    if ((Test-Path $path) -and (Test-RepoGitVersionSupported -GitExe $path)) {
        return $path
    }
    return $null
}

function Initialize-RepoUtf8Console {
    <#
    .SYNOPSIS
        Set the session console encodings to UTF-8.

    .DESCRIPTION
        Windows-only chcp.com is skipped when SystemRoot is empty so Linux and Dev Container hosts do not Join-Path a missing env var.
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'OutputEncoding preference must be session-scoped so native npm/node pipes decode UTF-8')]
    param()

    # Windows PowerShell 5.1 decodes native-command stdout with [Console]::OutputEncoding
    # (OEM 437 by default). Node/Vitest writes UTF-8 checkmarks (U+2713 = bytes E2 9C 93),
    # which OEM renders as Gamma-pound-o. UTF-8 + code page 65001 keeps the glyph intact
    # in the Cursor terminal capture.
    $utf8 = New-Object System.Text.UTF8Encoding $false
    [Console]::InputEncoding = $utf8
    [Console]::OutputEncoding = $utf8
    $global:OutputEncoding = $utf8

    if (-not [string]::IsNullOrWhiteSpace($env:SystemRoot)) {
        $chcp = Join-Path $env:SystemRoot 'System32\chcp.com'
        if (Test-Path -LiteralPath $chcp) {
            & $chcp 65001 | Out-Null
        }
    }
}

function Initialize-RepoShellEnvironment {
    <#
    .SYNOPSIS
        Resolve git and Node for harness scripts on the current host.

    .DESCRIPTION
        Windows-only PortableGit and fnm fallbacks are skipped when LOCALAPPDATA or APPDATA are empty.
    #>

    [CmdletBinding()]
    param(
        [string]$LogPrefix = 'repo-runtime'
    )

    Initialize-RepoUtf8Console

    $gitCmd = Get-Command git -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    $gitExe = if ($gitCmd) { [string]$gitCmd.Source } else { '' }
    if ($gitCmd -and (Test-RepoGitVersionSupported -GitExe $gitExe)) {
        Write-Output "[$LogPrefix] Using git: $gitExe ($(git --version))"
    }
    else {
        if ($gitCmd) {
            Write-Output "[$LogPrefix] PATH git unsupported: $gitExe ($(git --version))"
        }
        $portableGit = Resolve-RepoPortableGitExe
        if ($portableGit) {
            $gitDir = Split-Path $portableGit -Parent
            $env:PATH = "$gitDir;$env:PATH"
            Write-Output "[$LogPrefix] Using PortableGit: $portableGit ($(& $portableGit --version))"
        }
        else {
            Write-Output "[$LogPrefix] Git >=2.32 not found; lint-staged and some hooks may fail."
        }
    }

    $nodeCmd = Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    $nodeExe = if ($nodeCmd) { [string]$nodeCmd.Source } else { '' }
    if ($nodeCmd -and (Test-RepoNodeVersionSupported -NodeExe $nodeExe)) {
        Write-Output "[$LogPrefix] Using node: $nodeExe ($(node -v))"
        return
    }

    if ($nodeCmd) {
        Write-Output "[$LogPrefix] PATH node unsupported: $nodeExe ($(node -v))"
    }
    else {
        Write-Output "[$LogPrefix] Node.js not found in PATH."
    }

    $fnmBase = $null
    if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) {
        $fnmBase = Join-Path $env:APPDATA 'fnm\node-versions'
    }
    if ([string]::IsNullOrWhiteSpace($fnmBase) -or -not (Test-Path $fnmBase)) {
        throw 'Node >=24 required (package.json engines). Install via fnm or fix PATH.'
    }

    $fallback = Get-ChildItem -Path (Join-Path $fnmBase 'v*\installation\node.exe') -ErrorAction SilentlyContinue |
        Sort-Object { [version]($_.Directory.Parent.Name.TrimStart('v')) } |
        Where-Object { Test-RepoNodeVersionSupported -NodeExe $_.FullName } |
        Select-Object -Last 1

    if (-not $fallback) {
        throw "No supported fnm Node >=24 under $fnmBase"
    }

    $nodeDir = $fallback.Directory.FullName
    $env:PATH = "$nodeDir;$env:PATH"
    Write-Output "[$LogPrefix] Using fnm node: $($fallback.FullName) ($(& $fallback.FullName -v))"
}

function Invoke-RepoNpmScript {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$RepoRoot,
        [Parameter(Mandatory = $true)][string]$Name,
        [string[]]$NpmArguments = @(),
        [string]$LogPrefix = 'repo-runtime'
    )

    Push-Location $RepoRoot
    try {
        Initialize-RepoShellEnvironment -LogPrefix $LogPrefix | Out-Null
        $pkg = Get-Content 'package.json' -Raw | ConvertFrom-Json
        if (-not $pkg.scripts.PSObject.Properties[$Name]) {
            throw "package.json has no script '$Name'"
        }
        Write-Output "[$LogPrefix] npm run $Name$(if ($NpmArguments.Count) { " -- $($NpmArguments -join ' ')" })"
        if ($NpmArguments.Count -gt 0) {
            & npm run $Name -- @NpmArguments
        }
        else {
            & npm run $Name
        }
        if ($LASTEXITCODE -ne 0) {
            throw "npm run $Name failed with exit code $LASTEXITCODE"
        }
    }
    finally {
        Pop-Location
    }
}

Export-ModuleMember -Function Initialize-RepoUtf8Console, Initialize-RepoShellEnvironment, Invoke-RepoNpmScript, Test-RepoNodeVersionSupported, Test-RepoGitVersionSupported