Private/Provision.ps1

<# Provision helpers (re-written minimal to resolve parse issues) #>

function Get-GIAliasCanonicalHost {
  param([string]$Alias,[string]$Email)
  if (-not $Alias) { return 'unknown' }
  $a = $Alias.ToLowerInvariant()
  # Buscar en el mapa de plataformas
  foreach ($platform in $script:GIPlatformMap.Keys) {
    if ($a -match $platform) { 
      return $script:GIPlatformMap[$platform].CanonicalHost 
    }
  }
  # Fallback: extraer del email
  if ($Email -and $Email -match '@(.+)$') { return $Matches[1] }
  'unknown'
}

function Set-GIAliasGitConfig {
  param(
    [string]$UserHome,
    [string]$Alias,
    [string]$Name,
    [string]$Email,
    [string]$Username,
    [string]$SshKeyPath,
    [string[]]$SshUser,
    [switch]$DryRun
  )
  if (-not $UserHome -or -not $Alias) { return }
  $file = Join-Path $UserHome ('.gitconfig-' + $Alias)
  $h = Get-GIAliasCanonicalHost -Alias $Alias -Email $Email
  $credUrl = 'https://' + $h + '/'
  $lines = @()
  $lines += '# managed-by: gitidentities-module alias=' + $Alias + ' section=user'
  $lines += '[user]'
  $lines += ' name = ' + $Name
  $lines += ' email = ' + $Email
  $lines += '# managed-by: gitidentities-module alias=' + $Alias + ' section=credential'
  $lines += '[credential "' + $credUrl + '"]'
  $lines += ' username = ' + $Username
  if ($SshKeyPath -and $SshUser -and $SshUser.Count -gt 0) {
    $lines += '# managed-by: gitidentities-module alias=' + $Alias + ' section=core'
    $lines += '[core]'
    foreach ($user in $SshUser) {
      $lines += ' sshCommand = ssh -i ' + $SshKeyPath + ' -o User=' + $user + ' -o IdentitiesOnly=yes'
    }
  }
  $content = [string]::Join([Environment]::NewLine,$lines)
  Set-GIFileContentIfChanged -Path $file -Content $content -DryRun:$DryRun | Out-Null
  return $file
}

function Set-GIIncludeIfBlocks {
  param([string]$UserHome,[string]$Alias,[string[]]$Folders,[switch]$DryRun)
  if (-not $Folders -or $Folders.Count -eq 0) { return }
  $global = Join-Path $UserHome '.gitconfig'
  if (-not (Test-Path -LiteralPath $global) -and -not $DryRun) { '' | Out-File -FilePath $global -Encoding utf8 -NoNewline }
  $existing = if (Test-Path -LiteralPath $global) { Get-Content -LiteralPath $global -Encoding UTF8 } else { @() }
  foreach ($folder in $Folders) {
    $norm = Get-GINormalizedFolderPath -Path $folder
    if (-not $norm) { continue }
    $marker = "$script:GIManagedMarkerPrefix alias=$Alias folder=$norm"
    if ($existing | Where-Object { $_ -like "*$marker*" }) { Write-GILog -Level DEBUG -Message "includeIf exists for $Alias -> $norm"; continue }
    $block = @()
    $block += $marker
    $block += '[includeIf "gitdir:' + $norm + '"]'
    $block += ' path = ~/.gitconfig-' + $Alias
    if ($DryRun) { 
      Write-GILog -Level CHANGE -Message "[DryRun] Would append includeIf for $Alias -> $norm"
    } else { 
      # Asegurar que hay una línea vacía antes del bloque si el archivo no está vacío
      $needsNewline = $false
      if (Test-Path -LiteralPath $global) {
        $rawContent = [IO.File]::ReadAllText($global, [System.Text.UTF8Encoding]::new($false))
        if ($rawContent.Length -gt 0 -and -not $rawContent.EndsWith([Environment]::NewLine)) {
          $needsNewline = $true
        }
      }
      
      if ($needsNewline) {
        Add-Content -LiteralPath $global -Value "" -NoNewline:$false
      }
      
      foreach ($line in $block) {
        Add-Content -LiteralPath $global -Value $line
      }
      
      Write-GILog -Level CHANGE -Message "Added includeIf for $Alias -> $norm"
    }
  }
}

function Remove-GIIncludeIfBlocks {
  param([string]$UserHome,[string]$Alias,[string[]]$Folders,[switch]$All,[switch]$DryRun)
  $global = Join-Path $UserHome '.gitconfig'
  if (-not (Test-Path -LiteralPath $global)) { return }
  $lines = Get-Content -LiteralPath $global -Encoding UTF8
  $targets = @(); if (-not $All -and $Folders) { $targets = $Folders | ForEach-Object { Get-GINormalizedFolderPath -Path $_ } }
  $out=@(); $removed=0
  for ($i=0; $i -lt $lines.Count; $i++) {
    $l = $lines[$i]
    if ($l -like "*$script:GIManagedMarkerPrefix*" -and $l -like "*alias=$Alias*") {
      $match=$true
      if (-not $All -and $targets.Count -gt 0) { $match=$false; foreach($t in $targets){ if($l -like "*folder=$t*"){$match=$true;break} } }
      if ($match) { 
        # Saltar las siguientes 2 líneas del bloque (línea includeIf y path)
        $i+=2
        $removed++
        $msg = if($DryRun){"[DryRun] Would remove includeIf block for $Alias"} else {"Removed includeIf block for $Alias"}
        Write-GILog -Level CHANGE -Message $msg
        continue 
      }
    }
    $out += $l
  }
  if ($removed -gt 0 -and -not $DryRun) { [IO.File]::WriteAllText($global,[string]::Join([Environment]::NewLine,$out),[System.Text.UTF8Encoding]::new($false)) }
}

function Set-GISshKey {
  param([string]$UserHome,[string]$Alias,[string]$Email,[string]$Algorithm,[switch]$Force,[switch]$DryRun)
  $sshDir = Join-Path $UserHome '.ssh'
  $privateKey = Join-Path $sshDir ("id_" + $Alias)
  $publicKey  = $privateKey + '.pub'
  $exists = (Test-Path -LiteralPath $privateKey) -and (Test-Path -LiteralPath $publicKey)
  if ($exists -and -not $Force) {
    Write-GILog -Level DEBUG -Message "SSH key already exists: $privateKey"
    return $privateKey
  }
  if ($exists -and $Force) {
    if ($DryRun) { Write-GILog -Level CHANGE -Message "[DryRun] Would regenerate SSH key for $Alias"; return $privateKey }
    try { Remove-Item -LiteralPath $privateKey -Force -ErrorAction Stop; Remove-Item -LiteralPath $publicKey -Force -ErrorAction SilentlyContinue } catch { Write-GILog -Level WARN -Message "Failed deleting old key for regen: $($_.Exception.Message)" }
    Write-GILog -Level INFO -Message "Regenerating SSH key for $Alias"
  } elseif (-not $exists) {
    if ($DryRun) { Write-GILog -Level CHANGE -Message "[DryRun] Would generate SSH key: $privateKey"; return $privateKey }
    Write-GILog -Level INFO -Message "Generating SSH key for $Alias"
  }
  if (-not (Get-Command ssh-keygen -ErrorAction SilentlyContinue)) { Write-GILog -Level ERROR -Message 'ssh-keygen not found in PATH'; return $null }
  try {
    if (-not (Test-Path -LiteralPath $sshDir)) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null }
    $cmd = (Get-Command ssh-keygen -ErrorAction SilentlyContinue).Source
    $algo = if ($Algorithm) { $Algorithm.ToLowerInvariant() } else { 'ed25519' }
    if ($algo -eq 'rsa') {
      $argsPrimary = @('-t','rsa','-b','4096','-C',$Email,'-f',$privateKey,'-N','')
    } else {
      $argsPrimary = @('-t','ed25519','-C',$Email,'-f',$privateKey,'-N','')
    }
    Write-GILog -Level DEBUG -Message ("Invoking ssh-keygen primary: {0} {1}" -f $cmd, ($argsPrimary -join ' '))
    $primaryOutput = ssh-keygen @argsPrimary 2>&1
    $primaryOutput | ForEach-Object { $line=$_.ToString().Trim(); if ($line) { Write-GILog -Level DEBUG -Message "ssh-keygen: $line" } }
    $success = ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $privateKey))
    if (-not $success) {
      Write-GILog -Level WARN -Message "Primary ssh-keygen attempt failed (exit $LASTEXITCODE). Trying fallback via cmd.exe"
      $quotedEmail = '"' + $Email + '"'
      $quotedKey = '"' + $privateKey + '"'
      if ($algo -eq 'rsa') {
        $cmdLine = "ssh-keygen -t rsa -b 4096 -C $quotedEmail -f $quotedKey -N `"`"" 
      } else {
        $cmdLine = "ssh-keygen -t ed25519 -C $quotedEmail -f $quotedKey -N `"`"" 
      }
      Write-GILog -Level DEBUG -Message "Fallback command: $cmdLine"
      $fallbackOutput = cmd /c $cmdLine 2>&1
      $fallbackOutput | ForEach-Object { $line=$_.ToString().Trim(); if ($line) { Write-GILog -Level DEBUG -Message "ssh-keygen(fallback): $line" } }
      if (-not (Test-Path -LiteralPath $privateKey) -and $algo -ne 'rsa') {
        Write-GILog -Level WARN -Message "Fallback did not create ed25519 key. Trying RSA 4096"
        $cmdLine2 = "ssh-keygen -t rsa -b 4096 -C $quotedEmail -f $quotedKey -N `"`""
        Write-GILog -Level DEBUG -Message "RSA fallback command: $cmdLine2"
        $rsaOutput = cmd /c $cmdLine2 2>&1
        $rsaOutput | ForEach-Object { $line=$_.ToString().Trim(); if ($line) { Write-GILog -Level DEBUG -Message "ssh-keygen(rsa): $line" } }
      }
    }
    if ((Test-Path -LiteralPath $privateKey)) { Write-GILog -Level CHANGE -Message "SSH key ensured: $privateKey" } else { Write-GILog -Level ERROR -Message "Failed to generate SSH key for $Alias (check permissions/OpenSSH)" }
  } catch {
    Write-GILog -Level ERROR -Message "SSH key generation exception: $($_.Exception.Message)"
  }
  return $privateKey
}

function Set-GISshHostBlock {
  param([string]$UserHome,[string]$Alias,[string]$Platform,[switch]$DryRun)
  $sshDir = Join-Path $UserHome '.ssh'
  $cfg = Join-Path $sshDir 'config'
  if (-not (Test-Path -LiteralPath $sshDir) -and -not $DryRun) { New-Item -ItemType Directory -Path $sshDir | Out-Null }
  $existing = if (Test-Path -LiteralPath $cfg) { Get-Content -LiteralPath $cfg -Encoding UTF8 } else { @() }
  $marker = "$script:GIManagedMarkerPrefix alias=$Alias"
  $clean=@()
  for($i=0;$i -lt $existing.Count;$i++){
    if ($existing[$i] -like "*$marker*") { $i++; while($i -lt $existing.Count -and $existing[$i] -notmatch '^Host '){$i++}; $i--; continue }
    $clean += $existing[$i]
  }
  $platKey = if ($Platform) { $Platform.ToLowerInvariant() } else { 'github' }
  $hostName = if ($script:GIPlatformMap.ContainsKey($platKey)) { $script:GIPlatformMap[$platKey].HostName } else { 'github.com' }
  $block=@()
  $block += $marker
  $block += 'Host ' + $Alias
  $block += ' HostName ' + $hostName
  $block += ' IdentityFile ~/.ssh/id_' + $Alias
  $block += ' User git'
  $block += ' IdentitiesOnly yes'
  $text = [string]::Join([Environment]::NewLine,$block)
  if ($DryRun) { Write-GILog -Level CHANGE -Message "[DryRun] Would set SSH host block for $Alias" } else { $clean += $block; [IO.File]::WriteAllText($cfg,[string]::Join([Environment]::NewLine,$clean),[System.Text.UTF8Encoding]::new($false)); Write-GILog -Level CHANGE -Message "SSH host block set for $Alias" }
}

function Remove-GISshHostBlock {
  param([string]$UserHome,[string]$Alias,[switch]$DryRun)
  $sshDir = Join-Path $UserHome '.ssh'
  $cfg = Join-Path $sshDir 'config'
  if (-not (Test-Path -LiteralPath $cfg)) { return }
  $marker = "$script:GIManagedMarkerPrefix alias=$Alias"
  $lines = Get-Content -LiteralPath $cfg -Encoding UTF8
  $out=@(); $removed=$false
  for($i=0;$i -lt $lines.Count;$i++){
    if ($lines[$i] -like "*$marker*") { $removed=$true; $i++; while($i -lt $lines.Count -and $lines[$i] -notmatch '^Host '){$i++}; $i--; continue }
    $out += $lines[$i]
  }
  if ($removed) { if ($DryRun) { Write-GILog -Level CHANGE -Message "[DryRun] Would remove SSH host block for $Alias" } else { [IO.File]::WriteAllText($cfg,[string]::Join([Environment]::NewLine,$out),[System.Text.UTF8Encoding]::new($false)); Write-GILog -Level CHANGE -Message "Removed SSH host block for $Alias" } }
}