scripts/registry-add.ps1

#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Add an enterprise extension registry to dotbot.

.DESCRIPTION
    Registers a git-compatible repository as a dotbot extension registry.
    For local paths, creates a symlink. For git URLs, shallow-clones.
    Validates registry.yaml exists, parses, name matches, content is valid.

.PARAMETER Name
    Registry namespace (e.g., "myorg"). Must match the name field in registry.yaml.

.PARAMETER Source
    Local path or git URL to the registry repo.

.PARAMETER Branch
    Git branch to clone (default: main). Only used for git URLs.

.PARAMETER Force
    Overwrite existing registry with the same name.

.EXAMPLE
    registry-add.ps1 -Name myorg -Source C:\repos\myorg-dotbot-extensions
    registry-add.ps1 -Name myorg -Source https://github.com/org/dotbot-extensions.git
#>


[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$Name,
    [Parameter(Mandatory)][string]$Source,
    [string]$Branch = "main",
    [switch]$Force
)

$ErrorActionPreference = "Stop"

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

# Import platform functions (required for theme helpers)
$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: Add"

# ---------------------------------------------------------------------------
# 1. Check if registry already exists
# ---------------------------------------------------------------------------
if ((Test-Path $RegistryPath) -and -not $Force) {
    Write-DotbotError "Registry '$Name' already exists at $RegistryPath"
    Write-DotbotWarning "Use -Force to overwrite"
    exit 1
}

if ((Test-Path $RegistryPath) -and $Force) {
    Write-DotbotWarning "Removing existing registry '$Name'"
    Remove-Item -Path $RegistryPath -Recurse -Force
}

# ---------------------------------------------------------------------------
# 2. Ensure registries directory exists
# ---------------------------------------------------------------------------
if (-not (Test-Path $RegistriesDir)) {
    New-Item -Path $RegistriesDir -ItemType Directory -Force | Out-Null
}

# ---------------------------------------------------------------------------
# 3. Link or clone the source
# ---------------------------------------------------------------------------
$isLocalPath = Test-Path $Source
$isGitUrl = $Source -match '^https?://|^git@|^ssh://|\.git$'

if ($isLocalPath) {
    $resolvedSource = (Resolve-Path $Source).Path
    Write-Status "Creating symlink: $RegistryPath -> $resolvedSource"

    # On Windows, New-Item -ItemType Junction works without elevation (unlike SymbolicLink)
    if ($IsWindows) {
        New-Item -ItemType Junction -Path $RegistryPath -Target $resolvedSource | Out-Null
    } else {
        New-Item -ItemType SymbolicLink -Path $RegistryPath -Target $resolvedSource | Out-Null
    }
    Write-Success "Linked registry '$Name' from local path"

} elseif ($isGitUrl) {
    Write-Status "Cloning $Source (branch: $Branch) to $RegistryPath"
    $cloneOutput = & git clone --depth 1 --branch $Branch $Source $RegistryPath 2>&1
    if ($LASTEXITCODE -ne 0) {
        $errText = ($cloneOutput | Out-String).Trim()
        Write-DotbotError "Clone failed"
        Write-BlankLine
        if ($errText -match 'Authentication failed|401|403|could not read Username|terminal prompts disabled') {
            Write-DotbotWarning "The repository requires authentication. Ensure git can access it:"
            Write-BlankLine
            if ($Source -match 'github\.com') {
                Write-Status "GitHub: gh auth login"
                Write-DotbotCommand " git credential-manager configure"
            } elseif ($Source -match 'dev\.azure\.com') {
                Write-Status "Azure DevOps: az login"
                Write-DotbotCommand " git config credential.helper manager"
            } elseif ($Source -match 'gitlab') {
                Write-Status "GitLab: Add SSH key or set a PAT in ~/.netrc"
            } else {
                Write-Status "Ensure your git credential helper is configured or use SSH"
            }
            Write-BlankLine
            Write-DotbotCommand "Verify manually: git clone $Source /tmp/test-clone"
        } 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. Try -Branch main or -Branch master"
        } else {
            Write-DotbotCommand "$errText"
        }
        exit 1
    }
    Write-Success "Cloned registry '$Name' from $Source"

} else {
    Write-DotbotError "Source '$Source' is neither a valid local path nor a git URL"
    exit 1
}

# ---------------------------------------------------------------------------
# 4. Validate registry.yaml
# ---------------------------------------------------------------------------
Write-BlankLine
Write-DotbotSection -Title "VALIDATION"

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

# 4a. File must exist
if (-not (Test-Path $registryYamlPath)) {
    Write-DotbotError "registry.yaml not found in $RegistryPath"
    Write-DotbotWarning "Enterprise registries must have a registry.yaml at the root"
    # Clean up
    Remove-Item -Path $RegistryPath -Recurse -Force
    exit 1
}
Write-Success "registry.yaml found"

# 4b. Must parse
try {
    # Simple YAML parsing (same approach as init-project.ps1 Read-ProfileYaml)
    $registryMeta = @{}
    $contentSection = $null
    Get-Content $registryYamlPath | ForEach-Object {
        if ($_ -match '^\s*(name|display_name|description|version|min_dotbot_version)\s*:\s*(.+)$') {
            $registryMeta[$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) { $registryMeta['content'] = $contentSection }
} catch {
    Write-DotbotError "Failed to parse registry.yaml: $_"
    Remove-Item -Path $RegistryPath -Recurse -Force
    exit 1
}
Write-Success "registry.yaml parses correctly"

# 4c. Name must match
if ($registryMeta['name'] -ne $Name) {
    Write-DotbotError "Name mismatch: registry.yaml says '$($registryMeta['name'])', expected '$Name'"
    Remove-Item -Path $RegistryPath -Recurse -Force
    exit 1
}
Write-Success "Name matches: '$Name'"

# 4d. Content must list at least one item
$contentMap = $registryMeta['content']
if (-not $contentMap -or $contentMap.Count -eq 0) {
    Write-DotbotError "registry.yaml 'content' section is empty or missing"
    Remove-Item -Path $RegistryPath -Recurse -Force
    exit 1
}
$totalContent = 0
foreach ($key in $contentMap.Keys) {
    $totalContent += @($contentMap[$key]).Count
}
Write-Success "Content declares $totalContent item(s)"

# 4e. Verify referenced directories exist (warn only, non-fatal)
$contentTypeMap = @{
    "workflows" = "workflows"
    "stacks"    = "stacks"
    "tools"     = "tools"
    "skills"    = "skills"
    "agents"    = "agents"
}
$missingDirs = @()
foreach ($type in $contentMap.Keys) {
    $dirPrefix = $contentTypeMap[$type]
    if (-not $dirPrefix) { $dirPrefix = $type }
    foreach ($item in $contentMap[$type]) {
        $itemDir = Join-Path $RegistryPath "$dirPrefix\$item"
        if (-not (Test-Path $itemDir)) {
            $missingDirs += "$dirPrefix/$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"
}

# 4f. Check min_dotbot_version (warn only)
if ($registryMeta['min_dotbot_version']) {
    Write-DotbotCommand "Min dotbot version: $($registryMeta['min_dotbot_version'])"
}

# ---------------------------------------------------------------------------
# 5. Update registries.json
# ---------------------------------------------------------------------------
$config = @{ registries = @() }
if (Test-Path $ConfigPath) {
    try {
        $config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
        if (-not $config.registries) { $config.registries = @() }
    } catch {
        $config = @{ registries = @() }
    }
}

# Remove existing entry for this name
$config.registries = @($config.registries | Where-Object { $_.name -ne $Name })

# Add new entry
$entry = @{
    name        = $Name
    source      = if ($isLocalPath) { $resolvedSource } else { $Source }
    type        = if ($isLocalPath) { "local" } else { "git" }
    branch      = $Branch
    added_at    = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
    auto_update = (-not $isLocalPath)
}
$config.registries += $entry
$config | ConvertTo-Json -Depth 5 | Set-Content $ConfigPath
Write-Success "Updated registries.json"

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
Write-BlankLine
Write-DotbotBanner -Title "Registry '$Name' added successfully!"
Write-DotbotLabel -Label "Display name " -Value "$($registryMeta['display_name'])"
Write-DotbotLabel -Label "Version " -Value "$($registryMeta['version'])"
Write-DotbotLabel -Label "Path " -Value "$RegistryPath"
Write-BlankLine

# List available content
foreach ($type in $contentMap.Keys) {
    foreach ($item in $contentMap[$type]) {
        Write-Status "${Name}:${item} ($type)"
    }
}

Write-BlankLine
Write-DotbotCommand "Use in a new project: dotbot init -Workflow ${Name}:<workflow>"
Write-DotbotCommand "Add to existing project: dotbot workflow add ${Name}:<workflow>"
Write-BlankLine