Publish-PSLocalBase.ps1

<#
.SYNOPSIS
    Packages and publishes PSLocalBase to the PowerShell Gallery (PSGallery).

.DESCRIPTION
    Creates a clean, production-only staging package containing exclusively the
    core database engine and metadata files (PSLocalBase.psd1, PSLocalBase.psm1,
    InitWinSqlite3Native.ps1, LICENSE, README.md, CHANGELOG.md).
    
    Explicitly excludes development assets, tests, documentation, and the
    Playground visual GUI explorer.

.PARAMETER NuGetApiKey
    The PowerShell Gallery API key. If omitted, the script checks the
    PSGALLERY_API_KEY environment variable, or prompts securely for it.

.PARAMETER DryRun
    When specified, creates the staging directory, verifies the manifest,
    and lists the package contents without publishing to PSGallery.

.EXAMPLE
    # Test packaging without uploading:
    .\Publish-PSLocalBase.ps1 -DryRun

.EXAMPLE
    # Publish by passing API key directly:
    .\Publish-PSLocalBase.ps1 -NuGetApiKey "oy2a..."

.EXAMPLE
    # Publish using environment variable:
    $env:PSGALLERY_API_KEY = "oy2a..."
    .\Publish-PSLocalBase.ps1
#>


[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Position = 0)]
    [string]$NuGetApiKey,

    [switch]$DryRun
)

$ErrorActionPreference = 'Stop'

# Ensure TLS 1.2 is enabled for PowerShell Gallery (required for Windows PowerShell 5.1)
try {
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
} catch { }

# 1. Resolve root repository path
$repoRoot = $PSScriptRoot
if (-not $repoRoot) { $repoRoot = (Get-Location).Path }

$manifestPath = Join-Path $repoRoot "PSLocalBase.psd1"
if (-not (Test-Path $manifestPath)) {
    throw "Cannot find PSLocalBase.psd1 at '$repoRoot'. Run this script from the repository root."
}

# 2. Validate manifest syntax before staging
$manifest = Test-ModuleManifest -Path $manifestPath
$version = $manifest.Version.ToString()
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host " Preparing to Publish PSLocalBase v$version" -ForegroundColor Cyan
Write-Host "==========================================================" -ForegroundColor Cyan

# 3. Resolve API Key
$resolvedKey = $NuGetApiKey
if ([string]::IsNullOrWhiteSpace($resolvedKey)) {
    $resolvedKey = $env:PSGALLERY_API_KEY
}

if (-not $DryRun -and [string]::IsNullOrWhiteSpace($resolvedKey)) {
    Write-Host "No API key was passed as parameter or found in `$env:PSGALLERY_API_KEY." -ForegroundColor Yellow
    $secureKey = Read-Host -Prompt "Enter your PowerShell Gallery API Key" -AsSecureString
    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey)
    try {
        $resolvedKey = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
    } finally {
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
    }
}

if (-not $DryRun -and [string]::IsNullOrWhiteSpace($resolvedKey)) {
    throw "A valid PowerShell Gallery API key is required to publish. Use -DryRun to test packaging."
}

# 4. Prepare Staging Directory (Excluding Playground, _TESTS, _DOCS, .git)
$stagingId = [System.Guid]::NewGuid().ToString("N").Substring(0, 8)
# Note: PowerShellGet requires the leaf folder name to match the module name exactly ('PSLocalBase')
$stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) "PSLocalBase_Staging_$stagingId"
$stagingDir = Join-Path $stagingRoot "PSLocalBase"

try {
    if (Test-Path $stagingRoot) { Remove-Item $stagingRoot -Recurse -Force }
    New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null

    Write-Host "Staging production files..." -ForegroundColor Gray
    $filesToPackage = @(
        "PSLocalBase.psd1",
        "PSLocalBase.psm1",
        "InitWinSqlite3Native.ps1",
        "LICENSE",
        "README.md",
        "CHANGELOG.md"
    )

    foreach ($file in $filesToPackage) {
        $sourcePath = Join-Path $repoRoot $file
        if (-not (Test-Path $sourcePath)) {
            throw "Required package file not found: '$sourcePath'"
        }
        Copy-Item -Path $sourcePath -Destination $stagingDir -Force
        Write-Host " + Included: $file" -ForegroundColor DarkGreen
    }

    # Normalize README.md links in staging to prevent broken links
    # (replaces relative links to Playground, _DOCS, _TESTS with absolute GitHub URLs)
    $stagedReadme = Join-Path $stagingDir "README.md"
    if (Test-Path $stagedReadme) {
        $readmeText = [System.IO.File]::ReadAllText($stagedReadme, [System.Text.Encoding]::UTF8)

        # 1. Images: Playground/screenshots/... -> raw.githubusercontent.com
        $readmeText = $readmeText -replace '\(Playground/screenshots/([^\)]+)\)', '(https://raw.githubusercontent.com/cmartineztech/PSLocalBase/main/Playground/screenshots/$1)'

        # 2. Folders: (_DOCS/) or (_DOCS/#section) -> tree/main/_DOCS
        $readmeText = $readmeText -replace '\(_DOCS/(#[^\)]*)?\)', '(https://github.com/cmartineztech/PSLocalBase/tree/main/_DOCS/$1)'

        # 3. Specific documents in _DOCS, _TESTS, Playground
        $readmeText = $readmeText -replace '\(_DOCS/([a-zA-Z0-9_\-\.]+\.md)(#[^\)]*)?\)', '(https://github.com/cmartineztech/PSLocalBase/blob/main/_DOCS/$1$2)'
        $readmeText = $readmeText -replace '\(_TESTS/([^\)]+)\)', '(https://github.com/cmartineztech/PSLocalBase/blob/main/_TESTS/$1)'
        $readmeText = $readmeText -replace '\(Playground/([^\)]+)\)', '(https://github.com/cmartineztech/PSLocalBase/blob/main/Playground/$1)'
        $readmeText = $readmeText -replace '\(SECURITY\.md\)', '(https://github.com/cmartineztech/PSLocalBase/blob/main/SECURITY.md)'

        [System.IO.File]::WriteAllText($stagedReadme, $readmeText, [System.Text.Encoding]::UTF8)
        Write-Host " * Normalized staged README.md relative links to absolute GitHub URLs (no broken links)." -ForegroundColor DarkGreen
    }

    # Verify staging manifest
    $stagedManifestPath = Join-Path $stagingDir "PSLocalBase.psd1"
    $stagedManifest = Test-ModuleManifest -Path $stagedManifestPath
    Write-Host "[OK] Staged module manifest verified successfully (v$($stagedManifest.Version))." -ForegroundColor Green

    # Display package contents
    Write-Host "`nStaged Package Contents ($stagingDir):" -ForegroundColor Cyan
    Get-ChildItem -Path $stagingDir | Select-Object Name, Length | Format-Table -AutoSize | Out-String | Write-Host

    Write-Host "Excluded from package:" -ForegroundColor DarkGray
    Write-Host " - Playground/ (GUI explorer)" -ForegroundColor DarkGray
    Write-Host " - _TESTS/ (Unit and stress test suites)" -ForegroundColor DarkGray
    Write-Host " - _DOCS/ (Extended technical specifications)" -ForegroundColor DarkGray
    Write-Host " - .github/ (CI/CD workflows)" -ForegroundColor DarkGray

    # 5. Execute Publish or Dry Run
    if ($DryRun) {
        Write-Host "`n[DRY RUN COMPLETE] Package is valid and ready. No upload was performed." -ForegroundColor Yellow
        Write-Host "To publish for real, run:" -ForegroundColor Yellow
        Write-Host " .\Publish-PSLocalBase.ps1 -NuGetApiKey 'TU_API_KEY'" -ForegroundColor White
    } else {
        Write-Host "`nPublishing PSLocalBase v$version to PowerShell Gallery (PSGallery)..." -ForegroundColor Cyan
        Publish-Module -Path $stagingDir -NuGetApiKey $resolvedKey -Repository PSGallery -Verbose -ErrorAction Stop
        Write-Host "`n[SUCCESS] PSLocalBase v$version was published successfully to PowerShell Gallery!" -ForegroundColor Green
        Write-Host "View your published module at: https://www.powershellgallery.com/packages/PSLocalBase/$version" -ForegroundColor Cyan
        Write-Host "Install with: Install-Module -Name PSLocalBase" -ForegroundColor White
    }
} finally {
    if (Test-Path $stagingRoot) {
        Remove-Item $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
}