Private/PublishHelpers.ps1

# PublishHelpers.ps1
# Funciones helper compartidas para despliegues y publicación remota

<#
.SYNOPSIS
Extrae un valor de un archivo YAML simple.
 
.DESCRIPTION
Busca una clave en formato "key: value" y retorna el valor sin comillas.
No es un parser YAML completo, solo para casos simples.
 
.PARAMETER Content
Array de líneas del archivo YAML.
 
.PARAMETER Key
Nombre de la clave a buscar.
 
.EXAMPLE
Get-YamlValue $content 'name'
#>

function Get-YamlValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string[]]$Content,
        
        [Parameter(Mandatory=$true)]
        [string]$Key
    )
    
    $line = $Content | Where-Object { $_ -match "^\s*$Key\s*:" } | Select-Object -First 1
    if (-not $line) { return $null }
    
    $value = ($line -replace "^\s*$Key\s*:\s*", '').Trim()
    $value = $value -replace '^["'']|["'']$', ''  # Remover comillas
    return $value
}

<#
.SYNOPSIS
Lee un archivo .env y extrae todas las variables de entorno.
 
.DESCRIPTION
Parsea un archivo .env ignorando comentarios y líneas vacías.
Retorna un hashtable con las variables y extrae PORT si existe.
 
.PARAMETER Path
Ruta al archivo .env.
 
.PARAMETER DefaultPort
Puerto por defecto si no se encuentra PORT en .env (default: 8080).
 
.EXAMPLE
$config = Read-DotEnv "D:\proyecto\.env"
$envVars = $config.Env
$port = $config.Port
#>

function Read-DotEnv {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,
        
        [Parameter()]
        [int]$DefaultPort = 8080
    )
    
    $env = @{}
    $port = $DefaultPort
    
    if (-not (Test-Path $Path)) {
        return @{ Env = $env; Port = $port }
    }
    
    Get-Content $Path | Where-Object { $_ -and ($_ -notmatch '^\s*#') } | ForEach-Object {
        if ($_ -match '^\s*([^=]+)\s*=\s*(.*)$') {
            $key = $Matches[1].Trim()
            $value = $Matches[2].Trim()
            
            # Remover comillas exteriores
            if ($value -match '^["''](.+)["'']$') {
                $value = $Matches[1]
            }
            
            $env[$key] = $value
            
            # Extraer PORT si es numérico
            if ($key -eq 'PORT' -and $value -match '^\d+$') {
                $port = [int]$value
            }
        }
    }
    
    return @{ 
        Env = $env
        Port = $port
    }
}

<#
.SYNOPSIS
Valida y obtiene una distribución WSL disponible.
 
.DESCRIPTION
Verifica que WSL esté instalado, busca la distro preferida o hace fallback
a la primera distro Ubuntu disponible.
 
.PARAMETER Preferred
Nombre de la distro preferida (default: "Ubuntu").
 
.EXAMPLE
$distro = Get-ValidWSLDistro -Preferred "Ubuntu"
#>

function Get-ValidWSLDistro {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Preferred = "Ubuntu"
    )
    
    # Verificar que wsl.exe existe
    if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
        throw "wsl.exe no está disponible en PATH. Habilita WSL en Windows. Ejecuta 'wsl -l -v' para comprobar."
    }
    
    # Obtener listado de distros instaladas
    $wslListRaw = & wsl.exe --list --quiet 2>&1
    $distros = $wslListRaw -replace '\p{C}', '' -split '\r?\n' | 
               ForEach-Object { $_.Trim() } |
               Where-Object { $_ -ne '' }
    
    if (-not $distros -or $distros.Count -eq 0) {
        throw "No hay distribuciones WSL instaladas. Instala una con: wsl --install -d $Preferred"
    }
    
    # Verificar si la preferida existe
    if ($distros -contains $Preferred) {
        return $Preferred
    }
    
    # Fallback a cualquier Ubuntu
    $ubuntu = $distros | Where-Object { $_ -like 'Ubuntu*' } | Select-Object -First 1
    if ($ubuntu) {
        Write-Host "Advertencia: distro '$Preferred' no encontrada. Usando '$ubuntu' (fallback)." -ForegroundColor Yellow
        return $ubuntu
    }
    
    # Si no hay Ubuntu, fallar con información útil
    $available = $distros -join ', '
    throw "Distro '$Preferred' no encontrada. Distros instaladas: $available. Instala la correcta con: wsl --install -d $Preferred"
}

<#
.SYNOPSIS
Construye el string de variables de entorno para PM2.
 
.DESCRIPTION
Convierte un hashtable de variables de entorno en una cadena de parámetros
--env KEY='VALUE' para PM2, escapando comillas correctamente.
 
.PARAMETER EnvVars
Hashtable con las variables de entorno.
 
.EXAMPLE
$envString = New-PM2EnvString $EnvVars
# Resultado: "--env PORT='4321' --env DB_HOST='localhost' ..."
#>

function New-PM2EnvString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [hashtable]$EnvVars
    )
    
    if ($EnvVars.Count -eq 0) {
        return ""
    }
    
    $parts = @()
    foreach ($key in $EnvVars.Keys) {
        $value = $EnvVars[$key]
        # Escapar comillas simples para bash
        $escapedValue = $value -replace "'", "'\\''"
        $parts += "--env $key='$escapedValue'"
    }
    
    return $parts -join " "
}

<#
.SYNOPSIS
Crea un archivo temporal con contenido UTF-8 sin BOM y line endings Unix.
 
.DESCRIPTION
Helper para crear scripts bash temporales desde PowerShell asegurando
la codificación correcta (UTF-8 sin BOM, LF line endings).
 
.PARAMETER Content
Contenido del archivo.
 
.PARAMETER Prefix
Prefijo para el nombre del archivo temporal (default: "psdevops_").
 
.EXAMPLE
$tmpFile = New-UnixTempFile -Content $scriptContent -Prefix "build_"
#>

function New-UnixTempFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Content,
        
        [Parameter()]
        [string]$Prefix = "psdevops_"
    )
    
    # Normalizar a LF
    $unixContent = $Content -replace "`r`n", "`n" -replace "`r", "`n"
    
    # Crear archivo temporal
    $tmpPath = [IO.Path]::Combine([IO.Path]::GetTempPath(), "${Prefix}{0}.sh" -f ([guid]::NewGuid().ToString()))
    
    # Escribir como UTF-8 sin BOM
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($tmpPath, $unixContent, $utf8NoBom)
    
    return $tmpPath
}

<#
.SYNOPSIS
Ejecuta un script bash en el servidor remoto vía SSH.
 
.DESCRIPTION
Sube un script temporal al servidor remoto, lo ejecuta y lo elimina.
Maneja correctamente la codificación y limpieza de archivos temporales.
 
.PARAMETER ScriptContent
Contenido del script bash a ejecutar.
 
.PARAMETER User
Usuario SSH.
 
.PARAMETER IP
IP del servidor.
 
.PARAMETER Port
Puerto SSH.
 
.PARAMETER KeyPath
Ruta a la clave privada SSH.
 
.PARAMETER ScriptPrefix
Prefijo para el archivo temporal (default: "psdevops_remote_").
 
.EXAMPLE
Invoke-RemoteScript -ScriptContent $installScript -User "user" -IP "192.168.1.1" -Port 22 -KeyPath "~/.ssh/id_rsa"
#>

function Invoke-RemoteScript {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$ScriptContent,
        
        [Parameter(Mandatory=$true)]
        [string]$User,
        
        [Parameter(Mandatory=$true)]
        [string]$IP,
        
        [Parameter(Mandatory=$true)]
        [int]$Port,
        
        [Parameter(Mandatory=$true)]
        [string]$KeyPath,
        
        [Parameter()]
        [string]$ScriptPrefix = "psdevops_remote_"
    )
    
    $tmpLocal = New-UnixTempFile -Content $ScriptContent -Prefix $ScriptPrefix
    
    try {
        $remoteName = [IO.Path]::GetFileName($tmpLocal)
        $remotePath = "/tmp/$remoteName"
        
        # Subir script (suprimir salida)
        & scp -i $KeyPath -P $Port $tmpLocal "$($User)@$($IP):$remotePath" 2>&1 | Out-Null
        
        if ($LASTEXITCODE -ne 0) {
            throw "Error al subir script al servidor remoto (scp exit code: $LASTEXITCODE)"
        }
        
        # Ejecutar y eliminar (capturar salida completa)
        # IMPORTANTE: stderr (warnings) no son errores, solo el exit code != 0
        $remoteCmd = "bash $remotePath ; rc=`$?; rm -f $remotePath; exit `$rc"
        $ErrorActionPreference = 'Continue'  # Permitir stderr sin detener
        $output = & ssh -i $KeyPath -p $Port "$($User)@$($IP)" $remoteCmd 2>&1
        $exitCode = $LASTEXITCODE
        $ErrorActionPreference = 'Stop'  # Restaurar
        
        # Siempre mostrar salida (incluye warnings y mensajes informativos)
        if ($output) {
            $output | ForEach-Object { 
                $line = $_.ToString()
                # Colorear warnings en amarillo, errores en rojo, resto normal
                if ($line -match '^WARNING:') {
                    Write-Host $line -ForegroundColor Yellow
                } elseif ($line -match '^ERROR:') {
                    Write-Host $line -ForegroundColor Red
                } else {
                    Write-Host $line
                }
            }
        }
        
        return $exitCode
    }
    finally {
        Remove-Item -LiteralPath $tmpLocal -ErrorAction SilentlyContinue
    }
}

<#
.SYNOPSIS
Carga un script bash externo y reemplaza placeholders.
 
.DESCRIPTION
Lee un archivo .sh desde el directorio scripts/, reemplaza variables
tipo __PLACEHOLDER__ con valores reales, y retorna el contenido procesado.
 
.PARAMETER ScriptName
Nombre del archivo de script (ej: "Build-DartBinary.sh").
 
.PARAMETER Placeholders
Hashtable con los valores a reemplazar. Keys deben incluir __ antes y después.
 
.EXAMPLE
$script = Get-BashScript -ScriptName "Build-DartBinary.sh" -Placeholders @{
    '__WSLPROJECT__' = '/mnt/d/myproject'
    '__WSLWINOUT__' = '/mnt/c/temp/output'
}
#>

function Get-BashScript {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$ScriptName,
        
        [Parameter(Mandatory=$true)]
        [hashtable]$Placeholders
    )
    
    # Construir ruta al script
    $scriptPath = Join-Path $PSScriptRoot "scripts\$ScriptName"
    
    if (-not (Test-Path $scriptPath)) {
        throw "Script no encontrado: $scriptPath"
    }
    
    # Leer contenido
    $content = Get-Content $scriptPath -Raw
    
    # Reemplazar cada placeholder
    foreach ($key in $Placeholders.Keys) {
        $value = $Placeholders[$key]
        $content = $content -replace [regex]::Escape($key), $value
    }
    
    return $content
}