scripts/registry-update.ps1

#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Update one or all registered dotbot extension registries.

.DESCRIPTION
    For git-based registries, fetches and resets to the latest remote state.
    For local (symlinked) registries, re-validates registry.yaml.
    Re-validates content after update and records the update timestamp.

.PARAMETER Name
    Registry name to update. Omit to update all registered registries.

.PARAMETER Force
    For git registries, use 'git reset --hard' instead of '--ff-only' to
    discard any local changes.

.EXAMPLE
    registry-update.ps1
    registry-update.ps1 -Name myorg
    registry-update.ps1 -Name myorg -Force
#>


[CmdletBinding()]
param(
    [string]$Name,
    [switch]$Force
)

$ErrorActionPreference = "Stop"

$DotbotBase    = Join-Path $HOME "dotbot"
$RegistriesDir = Join-Path $DotbotBase "registries"
$ConfigPath    = Join-Path $DotbotBase "registries.json"

# Import platform functions
$PlatformFunctionsModule = Join-Path $PSScriptRoot "Platform-Functions.psm1"
if (-not (Test-Path $PlatformFunctionsModule)) {
    Write-Error "Required module not found: $PlatformFunctionsModule — run 'dotbot update' to repair"
    exit 1
}
Import-Module $PlatformFunctionsModule -Force -ErrorAction Stop

Write-DotbotBanner -Title "D O T B O T v3.5" -Subtitle "Registry: Update"

# ---------------------------------------------------------------------------
# 1. Read registries.json
# ---------------------------------------------------------------------------
if (-not (Test-Path $ConfigPath)) {
    Write-DotbotCommand "No registries configured."
    Write-BlankLine
    Write-DotbotWarning "Add one with: dotbot registry add <name> <source>"
    Write-BlankLine
    exit 0
}

$config = $null
try {
    $config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
} catch {
    Write-DotbotError "Failed to parse registries.json: $($_.Exception.Message)"
    exit 1
}

if (-not $config.registries -or $config.registries.Count -eq 0) {
    Write-DotbotCommand "No registries configured."
    Write-BlankLine
    Write-DotbotWarning "Add one with: dotbot registry add <name> <source>"
    Write-BlankLine
    exit 0
}

# ---------------------------------------------------------------------------
# 2. Select registries to update
# ---------------------------------------------------------------------------
if ($Name) {
    $targets = @($config.registries | Where-Object { $_.name -eq $Name })
    if ($targets.Count -eq 0) {
        Write-DotbotError "Registry '$Name' not found"
        Write-BlankLine
        Write-DotbotCommand "Run 'dotbot registry list' to see registered registries"
        Write-BlankLine
        exit 1
    }
} else {
    $targets = @($config.registries)
    Write-Status "Updating $($targets.Count) registry(ies)"
    Write-BlankLine
}

# ---------------------------------------------------------------------------
# Helper: validate registry.yaml (shared with registry-add.ps1 logic)
# ---------------------------------------------------------------------------
function Invoke-RegistryValidation {
    param([string]$RegistryPath, [string]$RegistryName)

    $registryYamlPath = Join-Path $RegistryPath "registry.yaml"

    if (-not (Test-Path $registryYamlPath)) {
        Write-DotbotError "registry.yaml not found in $RegistryPath"
        return $false
    }
    Write-Success "registry.yaml found"

    # Parse
    $meta = @{}
    $contentSection = $null
    try {
        Get-Content $registryYamlPath | ForEach-Object {
            if ($_ -match '^\s*(name|display_name|description|version|min_dotbot_version)\s*:\s*(.+)$') {
                $meta[$Matches[1]] = $Matches[2].Trim().Trim('"').Trim("'")
            }
            if ($_ -match '^\s*content\s*:') { $contentSection = @{} }
            if ($contentSection -and $_ -match '^\s+(workflows|stacks|tools|skills|agents)\s*:\s*\[(.+)\]') {
                $items = $Matches[2] -split ',' | ForEach-Object { $_.Trim().Trim('"').Trim("'") }
                $contentSection[$Matches[1]] = $items
            }
        }
        if ($contentSection) { $meta['content'] = $contentSection }
    } catch {
        Write-DotbotError "Failed to parse registry.yaml: $_"
        return $false
    }
    Write-Success "registry.yaml parses correctly"

    # Name must match
    if ($meta['name'] -ne $RegistryName) {
        Write-DotbotError "Name mismatch: registry.yaml says '$($meta['name'])', expected '$RegistryName'"
        return $false
    }
    Write-Success "Name matches: '$RegistryName'"

    # Content must declare at least one item
    $contentMap = $meta['content']
    if (-not $contentMap -or $contentMap.Count -eq 0) {
        Write-DotbotError "registry.yaml 'content' section is empty or missing"
        return $false
    }
    $totalContent = 0
    foreach ($key in $contentMap.Keys) { $totalContent += @($contentMap[$key]).Count }
    Write-Success "Content declares $totalContent item(s)"

    # Warn about missing directories (non-fatal)
    $missingDirs = @()
    foreach ($type in $contentMap.Keys) {
        foreach ($item in $contentMap[$type]) {
            $itemDir = Join-Path $RegistryPath "$type\$item"
            if (-not (Test-Path $itemDir)) { $missingDirs += "$type/$item" }
        }
    }
    if ($missingDirs.Count -gt 0) {
        foreach ($d in $missingDirs) { Write-DotbotWarning "Declared content directory not found: $d" }
    } else {
        Write-Success "All declared content directories exist"
    }

    # Surface version for display
    return $meta
}

# ---------------------------------------------------------------------------
# 3. Update each target
# ---------------------------------------------------------------------------
$updatedCount = 0
$failedCount  = 0

foreach ($entry in $targets) {
    $entryName    = $entry.name
    $registryPath = Join-Path $RegistriesDir $entryName

    Write-DotbotSection -Title "$entryName"

    if (-not (Test-Path $registryPath)) {
        Write-DotbotError "Registry directory not found: $registryPath"
        Write-DotbotWarning "Re-add with: dotbot registry add $entryName $($entry.source) --force"
        $failedCount++
        Write-BlankLine
        continue
    }

    $entrySucceeded = $false

    if ($entry.type -eq 'local') {
        # Local: symlink/junction is always current — just re-validate
        Write-Status "Local registry — re-validating (symlink tracks source automatically)"
        $result = Invoke-RegistryValidation -RegistryPath $registryPath -RegistryName $entryName
        if ($result -eq $false) {
            $failedCount++
        } else {
            Write-Success "Registry '$entryName' is valid"
            $updatedCount++
            $entrySucceeded = $true
        }

    } else {
        # Git: fetch + reset
        $branch = if ($entry.branch) { $entry.branch } else { 'main' }

        if ($Force) {
            Write-Status "Fetching $branch (force reset)"
            $fetchOutput = & git -C $registryPath fetch origin $branch 2>&1
            if ($LASTEXITCODE -ne 0) {
                $errText = ($fetchOutput | Out-String).Trim()
                Write-DotbotError "Fetch failed"
                Write-BlankLine
                if ($errText -match 'Authentication failed|401|403|could not read Username|terminal prompts disabled') {
                    Write-DotbotWarning "Authentication required — re-run: dotbot registry add $entryName $($entry.source) --force"
                } elseif ($errText -match 'not found|does not exist|404') {
                    Write-DotbotWarning "Repository not found. Check the URL and your access permissions."
                } else {
                    Write-DotbotCommand "$errText"
                }
                $failedCount++
                Write-BlankLine
                continue
            }
            $resetOutput = & git -C $registryPath reset --hard "origin/$branch" 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-DotbotError "Reset failed: $($resetOutput | Out-String)"
                $failedCount++
                Write-BlankLine
                continue
            }
        } else {
            Write-Status "Pulling $branch (fast-forward)"
            $pullOutput = & git -C $registryPath pull --ff-only origin $branch 2>&1
            if ($LASTEXITCODE -ne 0) {
                $errText = ($pullOutput | Out-String).Trim()
                Write-DotbotError "Pull failed"
                Write-BlankLine
                if ($errText -match 'Authentication failed|401|403|could not read Username|terminal prompts disabled') {
                    Write-DotbotWarning "Authentication required — re-run: dotbot registry add $entryName $($entry.source) --force"
                } elseif ($errText -match 'not found|does not exist|404') {
                    Write-DotbotWarning "Repository not found. Check the URL and your access permissions."
                } elseif ($errText -match "Remote branch.*not found|couldn't find remote ref") {
                    Write-DotbotWarning "Branch '$branch' not found on remote."
                } elseif ($errText -match 'Not possible to fast-forward|diverged') {
                    Write-DotbotWarning "Cannot fast-forward — use -Force to reset to remote state"
                } else {
                    Write-DotbotCommand "$errText"
                }
                $failedCount++
                Write-BlankLine
                continue
            }
        }

        Write-Success "Pulled latest from $($entry.source)"

        Write-BlankLine
        Write-DotbotSection -Title "VALIDATION"
        $result = Invoke-RegistryValidation -RegistryPath $registryPath -RegistryName $entryName
        if ($result -eq $false) {
            $failedCount++
        } else {
            $updatedCount++
            $entrySucceeded = $true
        }
    }

    # Record updated_at only when the update and validation both succeeded
    if ($entrySucceeded) {
        $config.registries = @($config.registries | ForEach-Object {
            if ($_.name -eq $entryName) {
                $_ | Add-Member -NotePropertyName 'updated_at' -NotePropertyValue ((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")) -Force
            }
            $_
        })
    }

    Write-BlankLine
}

# Persist updated timestamps
$config | ConvertTo-Json -Depth 5 | Set-Content $ConfigPath

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
Write-DotbotSection -Title "SUMMARY"
if ($updatedCount -gt 0) { Write-Success "$updatedCount registry(ies) updated" }
if ($failedCount -gt 0)  { Write-DotbotError "$failedCount registry(ies) failed" }
Write-BlankLine

if ($failedCount -gt 0) { exit 1 }