src/UI/Screens/ReleaseScreen.ps1
|
# ReleaseScreen — Dashboard de release flow (replica ReleaseFlowController v2). # # MVP enfocado en lo más usado: # - Create Tag (input nombre, annotated) # - Push All Tags # - Cancel # # Muestra arriba el estado: branch actual, latest tag, hasPackageJson + version. # Bump Version queda para iteración futura — requiere npm service que el v3 # todavía no tiene. class ReleaseScreen { [object] $Theme [object] $Renderer [object] $Primitives [object] $Frame [object] $AppHeader [object] $StatusBar [object] $Git [object] $I18n = $null # Inyectable post-construct. [object] $Repo [int] $SelectedIndex = 0 [string] $Mode = 'list' # 'list' | 'input-tag' [bool] $QuitRequested = $false [bool] $UseAltBuffer = $true # Estado calculado [string] $CurrentBranch = '' [string] $LatestTag = '' [int] $TagCount = 0 [bool] $HasPackageJson = $false [string] $CurrentVersion = '' [string] $InputBuffer = '' [string] $StatusMessage = '' [string] $StatusKind = '' ReleaseScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $git) { $this.Theme = $theme $this.Renderer = $renderer $this.Primitives = $primitives $this.Frame = $frame $this.AppHeader = $appHeader $this.StatusBar = $statusBar $this.Git = $git } [void] Open([object]$repo) { if ($null -eq $repo) { throw "ReleaseScreen.Open: repo no puede ser null." } $this.Repo = $repo $this.SelectedIndex = 0 $this.Mode = 'list' $this.StatusMessage = '' $this.StatusKind = '' $this.RefreshState() if ([Console]::IsInputRedirected) { $lines = $this.BuildLines() foreach ($l in $lines) { Write-Host $l } return } $errOut = [Console]::Error $this.Render($errOut) try { while ($true) { $key = [Console]::ReadKey($true) $exit = $this.HandleKey($key) if ($exit -eq 'back') { return } if ($exit -eq 'quit') { $this.QuitRequested = $true; return } $this.Render($errOut) } } finally { } } hidden [void] RefreshState() { $repoPath = $this.Repo.Path $this.Git.InvalidatePath($repoPath) $state = $this.Git.GetRepoState($repoPath) $this.CurrentBranch = $state.Branch $tags = @($this.Git.GetTags($repoPath)) $this.TagCount = $tags.Count $this.LatestTag = if ($tags.Count -gt 0) { $tags[0] } else { '(none)' } # Detectar package.json + version sin npm service (Get-Content directo). $pkg = Join-Path $repoPath 'package.json' if (Test-Path -LiteralPath $pkg) { $this.HasPackageJson = $true try { $json = Get-Content -LiteralPath $pkg -Raw -Encoding UTF8 | ConvertFrom-Json $this.CurrentVersion = if ($json.version) { [string]$json.version } else { '(no version field)' } } catch { $this.CurrentVersion = '(parse error)' } } else { $this.HasPackageJson = $false $this.CurrentVersion = '' } } [object[]] AvailableActions() { $items = @() $items += @{ Label = 'Create Tag'; Action = 'create-tag' } $items += @{ Label = 'Push All Tags'; Action = 'push-tags' } $items += @{ Label = 'Cancel'; Action = 'cancel' } return $items } hidden [void] Render([object]$errOut) { if ([Console]::IsInputRedirected) { return } $lines = $this.BuildLines() $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync() $errOut.Write($framePayload) $errOut.Flush() } hidden [string] HandleKey([System.ConsoleKeyInfo]$key) { switch ($this.Mode) { 'list' { return $this.HandleKeyList($key) } 'input-tag' { return $this.HandleKeyInputTag($key) } } return '' } hidden [string] HandleKeyList([System.ConsoleKeyInfo]$key) { $k = $key.Key $c = $key.KeyChar $items = $this.AvailableActions() if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' } if ($k -eq 'Escape' -or $k -eq 'LeftArrow' -or $c -eq 'h') { return 'back' } if ($k -eq 'UpArrow' -or $c -eq 'k') { if ($items.Count -gt 0) { $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $items.Count - 1 } else { $this.SelectedIndex - 1 } } return '' } if ($k -eq 'DownArrow' -or $c -eq 'j') { if ($items.Count -gt 0) { $this.SelectedIndex = if ($this.SelectedIndex -ge $items.Count - 1) { 0 } else { $this.SelectedIndex + 1 } } return '' } if ($k -eq 'Enter') { $action = $items[$this.SelectedIndex].Action switch ($action) { 'create-tag' { $this.OpenTagInput(); return '' } 'push-tags' { $this.DoPushTags(); return '' } 'cancel' { return 'back' } } } return '' } hidden [void] OpenTagInput() { $this.InputBuffer = '' $this.Mode = 'input-tag' } hidden [string] HandleKeyInputTag([System.ConsoleKeyInfo]$key) { $k = $key.Key $c = $key.KeyChar if ($k -eq 'Escape') { $this.InputBuffer = '' $this.Mode = 'list' return '' } if ($k -eq 'Enter') { $name = $this.InputBuffer.Trim() $this.InputBuffer = '' $this.Mode = 'list' if (-not $name) { $this.SetStatus('nombre del tag vacío — cancelado', 'error') return '' } $this.DoCreateTag($name) return '' } if ($k -eq 'Backspace') { if ($this.InputBuffer.Length -gt 0) { $this.InputBuffer = $this.InputBuffer.Substring(0, $this.InputBuffer.Length - 1) } return '' } if (-not [char]::IsControl($c)) { $this.InputBuffer += $c } return '' } hidden [void] DoCreateTag([string]$tagName) { $r = $this.Git.CreateTag($this.Repo.Path, $tagName, '') $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' }))) if ($r.Ok) { $this.RefreshState() } } hidden [void] DoPushTags() { $r = $this.Git.PushTags($this.Repo.Path) $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' }))) } hidden [void] SetStatus([string]$msg, [string]$kind) { $this.StatusMessage = $msg $this.StatusKind = $kind } [string[]] BuildLines() { $reset = [AnsiService]::Reset $r = $this.Renderer $lines = [System.Collections.Generic.List[string]]::new() $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' } $lines.Add($this.Frame.TitleBar('repo-nav', "Release · $repoName", '1.0.0')) $bc = [BreadcrumbBuilder]::Build($this.Repo.Path) $segs = @($bc.Segs) + @($bc.Current) + @('Branches') $lines.Add($this.AppHeader.Render($segs, 'Release', @())) $lines.Add($r.HRule()) # Status / prompt line if ($this.Mode -eq 'input-tag') { $cursor = $this.Theme.Fg('acc') + '_' + $reset $label = $this.Theme.Fg('fg2') + 'Nombre del tag (ej v1.2.3): ' + $reset $buf = $this.Theme.Fg('fg1') + $this.InputBuffer + $reset $lines.Add(' ' + $label + $buf + $cursor) } elseif ($this.StatusMessage) { $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' } $lines.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset) } else { $lines.Add('') } # State summary $summary = @() $summary += $this.Theme.Fg('fg2') + 'branch:' + $reset + ' ' + $this.Theme.Fg('acc') + $this.CurrentBranch + $reset $summary += $this.Theme.Fg('fg2') + 'latest tag:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.LatestTag + $reset $summary += $this.Theme.Fg('fg2') + 'tags:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.TagCount + $reset if ($this.HasPackageJson) { $summary += $this.Theme.Fg('fg2') + 'version:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.CurrentVersion + $reset } $lines.Add(' ' + ($summary -join ' · ')) $lines.Add($r.HRule()) # Actions $items = $this.AvailableActions() for ($i = 0; $i -lt $items.Count; $i++) { $sel = ($this.SelectedIndex -eq $i -and $this.Mode -eq 'list') $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' } $color = if ($items[$i].Action -eq 'cancel') { 'fg2' } elseif ($sel) { 'acc' } else { 'fg1' } $lines.Add($marker + $this.Theme.Fg($color) + $items[$i].Label + $reset) } $hints = $this.StatusBarHints() $lines.Add($r.HRule()) return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.Count) } hidden [string] T([string]$key) { if ($null -ne $this.I18n) { return $this.I18n.T($key) } return ([I18nService]::new('es')).T($key) } hidden [array] StatusBarHints() { switch ($this.Mode) { 'input-tag' { return @( @{ k = $this.T('hint.type'); label = $this.T('hint.save') } @{ k = '↵'; label = $this.T('hint.accept') } @{ k = 'Esc'; label = $this.T('hint.cancel') } ) } default { return @( @{ k = '↑↓'; label = $this.T('hint.move') } @{ k = '↵'; label = $this.T('hint.run') } @{ k = '←'; label = $this.T('hint.back') } @{ k = 'Q'; label = $this.T('hint.quit') } ) } } return @() } } |