Install.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Install / uninstall del comando `rnav` (alias del repo-nav v3) en el $PROFILE. .DESCRIPTION Idempotente: si el alias ya está instalado, no duplica. Detecta el path actual del repo (donde vive este script) y crea una function que dot-sourcea `repo-nav.ps1`. Verifica deps (pwsh 7+, git en PATH). Bloque agregado al $PROFILE entre marcadores BEGIN/END para que -Uninstall pueda quitarlo limpio. .PARAMETER Uninstall Quita la function `rnav` del $PROFILE. Restaura el archivo a su estado previo (sin el bloque del repo-nav). .PARAMETER ProfilePath Override del path del $PROFILE (útil para tests). Default: el $PROFILE de pwsh actual (current host). .PARAMETER Force Reescribe el bloque aunque ya esté instalado (útil después de mover el repo). .EXAMPLE .\Install.ps1 Instala el alias rnav apuntando a este repo. .EXAMPLE .\Install.ps1 -Uninstall Quita el alias. .EXAMPLE .\Install.ps1 -Force Reinstala (reescribe el bloque, útil después de git pull si la entry function cambió). .NOTES REQUIERE pwsh 7+ (NO Windows PowerShell 5.1). Lo chequea explícito. #> [CmdletBinding()] param( [switch] $Uninstall, [string] $ProfilePath, [switch] $Force, # Si se pasa, persiste el modo en settings.json sin preguntar interactivo. # Útil para `repo-nav init -AutoLoadMode None` automatizado. [ValidateSet('All', 'Favorites', 'None', '')] [string] $AutoLoadMode = '', # Skipea el prompt interactivo si no hay tty (CI, scripts). [switch] $NonInteractive, # Nombre del alias a registrar. Default: 'rnav'. ConfigScreen lo usa para # renombrar el alias existente (se llama install -AliasName <new> -Force). [string] $AliasName = 'rnav' ) if (-not ($AliasName -match '^[a-zA-Z][\w\-]*$')) { Write-Host "[error] Nombre de alias inválido: '$AliasName'. Debe empezar con letra y contener solo letras, dígitos, '-' o '_'." -ForegroundColor Red exit 1 } # Marcadores idempotentes — el bloque entre estos comments se reemplaza/quita. $BEGIN_MARKER = '# >>> repo-nav v3 — managed block (do not edit)' $END_MARKER = '# <<< repo-nav v3 — managed block end' function Write-Status { param([string]$Message, [string]$Kind = 'info') $color = switch ($Kind) { 'ok' { 'Green' } 'warn' { 'Yellow' } 'error' { 'Red' } default { 'Cyan' } } $prefix = switch ($Kind) { 'ok' { '[ok] ' } 'warn' { '[warn] ' } 'error' { '[error]' } default { '[info] ' } } Write-Host "$prefix $Message" -ForegroundColor $color } function Get-EffectiveProfilePath { param([string]$Override) if ($Override) { return $Override } if (-not $PROFILE) { throw "No hay `$PROFILE definido. ¿Estás en un host PS válido?" } return $PROFILE } function Test-Dependencies { $ok = $true # pwsh 7+ if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Status "PowerShell version: $($PSVersionTable.PSVersion). repo-nav v3 requiere pwsh 7+." 'error' $ok = $false } else { Write-Status "PowerShell $($PSVersionTable.PSVersion) OK." 'ok' } # git en PATH $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { Write-Status "git no está en el PATH. Instalalo desde https://git-scm.com" 'error' $ok = $false } else { $gitVer = (& git --version) -replace 'git version ', '' Write-Status "git $gitVer OK." 'ok' } return $ok } function Get-ManagedBlock { param([string]$RepoRoot, [string]$Name = 'rnav') $entryScript = Join-Path $RepoRoot 'repo-nav.ps1' # Quote-safe: usamos single-quotes en el path para evitar interpolación al # escribir, y dejamos que pwsh lo resuelva al runtime. El path nunca debería # contener single-quotes (paths Windows típicos no las tienen) — si llegara # a tenerlas, se rompería; lo validamos explícito. if ($entryScript -match "'") { throw "Path con caracteres no soportados (single quote): $entryScript" } $argsVar = "$($Name)Args" return @" $BEGIN_MARKER # Generado por Install.ps1 — para reinstalar: ejecutá Install.ps1 -Force. # Para quitar: Install.ps1 -Uninstall. function $Name { param([Parameter(ValueFromRemainingArguments)] `$$argsVar) & '$entryScript' @$argsVar } $END_MARKER "@ } function Read-ProfileSafely { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return '' } return [System.IO.File]::ReadAllText($Path, [System.Text.UTF8Encoding]::new($false)) } function Write-ProfileSafely { param([string]$Path, [string]$Content) # .NET puro — los cmdlets PS (Set-Content / New-Item) tienen quirks de # parameter sets con strings multi-línea que tiran ParameterBindingException. $dir = [System.IO.Path]::GetDirectoryName($Path) if ($dir -and -not [System.IO.Directory]::Exists($dir)) { [System.IO.Directory]::CreateDirectory($dir) | Out-Null } $utf8 = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($Path, [string]$Content, $utf8) } function Remove-ExistingBlock { param([string]$ProfileContent) $lines = $ProfileContent -split "`r?`n" $kept = @() $insideBlock = $false foreach ($line in $lines) { if ($line -eq $BEGIN_MARKER) { $insideBlock = $true; continue } if ($line -eq $END_MARKER) { $insideBlock = $false; continue } if (-not $insideBlock) { $kept += $line } } # Trim trailing newlines duplicados. $result = ($kept -join "`n") return $result.TrimEnd("`n", "`r") } function Test-BlockInstalled { param([string]$ProfileContent) return $ProfileContent.Contains($BEGIN_MARKER) } # Prompt interactivo para AutoLoadMode. Devuelve uno de 'All' | 'Favorites' | 'None'. # Si la sesión es non-interactive (input redirected) o se pasó -NonInteractive, # devuelve 'All' como default sin preguntar. function Read-AutoLoadModeInteractive { if ($NonInteractive -or [Console]::IsInputRedirected) { return 'All' } Write-Host '' Write-Host '¿Cómo querés cargar git status al iniciar repo-nav?' -ForegroundColor Cyan Write-Host ' Si tenés muchos repos o red lenta, podés posponer la carga.' Write-Host '' Write-Host ' [1] All — carga todos los repos al boot (default, UX más rico)' Write-Host ' [2] Favorites — solo los repos marcados como favoritos; el resto on-demand' Write-Host ' [3] None — ninguno; pulsás R o navegás con el cursor para cargar' Write-Host '' while ($true) { $choice = Read-Host 'Elegí 1, 2 o 3 (Enter = 1)' if ([string]::IsNullOrWhiteSpace($choice) -or $choice -eq '1') { return 'All' } if ($choice -eq '2') { return 'Favorites' } if ($choice -eq '3') { return 'None' } Write-Host " '$choice' no es válido. Elegí 1, 2 o 3." -ForegroundColor Yellow } } # Persiste AutoLoadMode en settings.json. Self-contained: NO carga el v3 entero # para evitar el costo de Bootstrap durante el install. Lee/escribe el JSON # directo via System.IO + ConvertTo-Json. function Save-AutoLoadModeToSettings { param([string]$Mode) # `$home` es read-only built-in en PS — no podemos pisarlo. Usamos otro nombre. $userHome = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } $settingsPath = Join-Path $userHome '.repo-nav/preferences.json' $settingsDir = Split-Path -Path $settingsPath -Parent if (-not [System.IO.Directory]::Exists($settingsDir)) { [System.IO.Directory]::CreateDirectory($settingsDir) | Out-Null } # Leer JSON existente o arrancar con hashtable vacío. $obj = $null if ([System.IO.File]::Exists($settingsPath)) { try { $raw = [System.IO.File]::ReadAllText($settingsPath, [System.Text.UTF8Encoding]::new($false)) if ($raw) { $obj = $raw | ConvertFrom-Json -AsHashtable } } catch { # JSON corrupto — empezamos de cero. El usuario perdió config rara # pero el install sigue. Si era importante, había que revisar antes. $obj = $null } } if (-not $obj) { $obj = @{} } $obj.AutoLoadMode = $Mode $json = $obj | ConvertTo-Json -Depth 10 $utf8 = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($settingsPath, $json, $utf8) } # ─── Main ─────────────────────────────────────────────────────────────────── $repoRoot = $PSScriptRoot $entryScript = Join-Path $repoRoot 'repo-nav.ps1' if (-not (Test-Path -LiteralPath $entryScript)) { Write-Status "No se encontró $entryScript. ¿Estás corriendo Install.ps1 desde la raíz del repo?" 'error' exit 1 } $profilePath = Get-EffectiveProfilePath -Override $ProfilePath $existing = Read-ProfileSafely -Path $profilePath $alreadyInstalled = Test-BlockInstalled -ProfileContent $existing # ─── Modo uninstall ───────────────────────────────────────────────────────── if ($Uninstall) { if (-not $alreadyInstalled) { Write-Status "El alias rnav no estaba instalado en $profilePath." 'warn' exit 0 } $cleaned = Remove-ExistingBlock -ProfileContent $existing Write-ProfileSafely -Path $profilePath -Content $cleaned Write-Status "Alias rnav removido de $profilePath." 'ok' Write-Status "Abrí una pwsh nueva para que el cambio tome efecto." 'info' exit 0 } # ─── Modo install ────────────────────────────────────────────────────────── Write-Status "Instalando repo-nav v3..." 'info' Write-Host '' # 1. Verificar dependencias if (-not (Test-Dependencies)) { Write-Status "Dependencias faltantes. Resolvé los errores y volvé a correr." 'error' exit 1 } # 2. Bloque a escribir (usa $AliasName, default 'rnav') $block = Get-ManagedBlock -RepoRoot $repoRoot -Name $AliasName # 3. Si ya está instalado y no hay -Force, salir. if ($alreadyInstalled -and -not $Force) { Write-Status "El alias $AliasName ya está instalado en $profilePath. Usá -Force para reescribir." 'warn' Write-Status "Repo path: $repoRoot" 'info' exit 0 } # 4. Limpiar bloque previo (si existe), después append el nuevo. $cleaned = Remove-ExistingBlock -ProfileContent $existing $separator = if ([string]::IsNullOrWhiteSpace($cleaned)) { '' } else { "`n`n" } $newContent = $cleaned + $separator + $block + "`n" Write-ProfileSafely -Path $profilePath -Content $newContent # 5. AutoLoadMode: si se pasó por flag, lo usamos directo. Si no, preguntamos # (a menos que sea non-interactive). Persistimos el resultado en settings.json # para que MainScreen lo lea al boot. $effectiveMode = if ($AutoLoadMode) { $AutoLoadMode } else { Read-AutoLoadModeInteractive } Save-AutoLoadModeToSettings -Mode $effectiveMode Write-Host '' Write-Status "Alias $AliasName instalado." 'ok' Write-Status "Profile: $profilePath" 'info' Write-Status "Repo: $repoRoot" 'info' Write-Status "AutoLoadMode: $effectiveMode" 'info' Write-Host '' Write-Status "Abrí una pwsh nueva y tipeá '$AliasName' para usar el v3." 'info' |