Update-ModifiedCmdletDocs.ps1

<#
.SYNOPSIS
Updates markdown documentation for modified PowerShell cmdlets based on git changes.

.DESCRIPTION
By default, this script finds Public cmdlet files changed in local commits that have not
been pushed to the upstream tracking branch, then updates their markdown documentation.

Use -ChangeSource WorkingTree to instead process staged and/or unstaged working tree changes.

.PARAMETER Path
The base path to the repository. Defaults to current directory.

.PARAMETER DocumentationPath
Path to the documentation directory. Defaults to "./Documentation/"

.PARAMETER PublicPath
Path to the Public cmdlets directory. Defaults to "./Public/"

.PARAMETER ChangeSource
Unpushed uses commits on HEAD that are not on the upstream branch. WorkingTree uses git status.

.PARAMETER IncludeStaged
When ChangeSource is WorkingTree, include staged files in addition to unstaged changes.

.PARAMETER RecreateUpdatedDocs
When true, existing markdown files are deleted and regenerated. When false, existing files are updated in place.

.PARAMETER RepairAllDocumentation
When true, repairs PlatyPS placeholder text in all markdown files under DocumentationPath and regenerates external help.

.EXAMPLE
.\Update-ModifiedCmdletDocs.ps1

.EXAMPLE
.\Update-ModifiedCmdletDocs.ps1 -ChangeSource WorkingTree

.EXAMPLE
.\Update-ModifiedCmdletDocs.ps1 -RepairAllDocumentation
#>


[CmdletBinding()]
param(
    [string]$Path = ".",
    [string]$DocumentationPath = "$PSScriptRoot\Documentation\",
    [string]$PublicPath = "$PSScriptRoot\Public\",
    [ValidateSet('Unpushed', 'WorkingTree')]
    [string]$ChangeSource = 'Unpushed',
    [bool]$IncludeStaged = $true,
    [bool]$RecreateUpdatedDocs = $true,
    [bool]$RepairAllDocumentation = $false
)

function Get-UnpushedGitRange {
    git rev-parse --abbrev-ref '@{u}' 2>$null | Out-Null
    if ($LASTEXITCODE -eq 0) {
        return '@{u}..HEAD'
    }

    $defaultRemoteBranch = git symbolic-ref refs/remotes/origin/HEAD 2>$null
    if ($defaultRemoteBranch -match 'origin/(.+)$') {
        return "origin/$($Matches[1])..HEAD"
    }

    return 'origin/main..HEAD'
}

function Get-ModifiedPublicCmdletsFromGitPaths {
    param(
        [Parameter(Mandatory)]
        [string[]]$ChangedPaths
    )

    return $ChangedPaths | Where-Object {
        $_ -replace '\\', '/' -match '^Public/[^/]+\.ps1$'
    } | ForEach-Object {
        $normalizedPath = $_ -replace '\\', '/'
        if ($normalizedPath -match 'Public/([^/]+)\.ps1$') {
            $Matches[1]
        }
    } | Select-Object -Unique
}

function Test-PlatyPSV1 {
    return $null -ne (Get-Command New-MarkdownCommandHelp -ErrorAction SilentlyContinue)
}

function New-FlatMarkdownCommandHelp {
    param(
        [Parameter(Mandatory)]
        [string]$CommandName,

        [Parameter(Mandatory)]
        [string]$OutputFolder,

        [Parameter(Mandatory)]
        [string]$TargetPath
    )

    if (Test-PlatyPSV1) {
        $commandInfo = Get-Command $CommandName -ErrorAction Stop
        New-MarkdownCommandHelp -CommandInfo $commandInfo -OutputFolder $OutputFolder -Force -ErrorAction Stop | Out-Null

        $moduleMarkdownPath = Join-Path $OutputFolder $commandInfo.Module.Name "$CommandName.md"
        if (-not (Test-Path $moduleMarkdownPath)) {
            throw "Expected markdown file was not created at '$moduleMarkdownPath'."
        }

        Move-Item -Path $moduleMarkdownPath -Destination $TargetPath -Force

        $moduleFolder = Join-Path $OutputFolder $commandInfo.Module.Name
        if (Test-Path $moduleFolder) {
            $remainingItems = @(Get-ChildItem -Path $moduleFolder -Force -ErrorAction SilentlyContinue)
            if ($remainingItems.Count -eq 0) {
                Remove-Item -Path $moduleFolder -Force -ErrorAction SilentlyContinue
            }
        }

        return
    }

    if (Get-Command New-MarkdownHelp -ErrorAction SilentlyContinue) {
        New-MarkdownHelp -Command $CommandName -OutputFolder $OutputFolder -ErrorAction Stop | Out-Null
        return
    }

    throw "PlatyPS is not installed. Install Microsoft.PowerShell.PlatyPS (recommended) or legacy platyPS."
}

function Update-FlatMarkdownCommandHelp {
    param(
        [Parameter(Mandatory)]
        [string]$MarkdownPath
    )

    if (Test-PlatyPSV1) {
        Update-MarkdownCommandHelp -Path $MarkdownPath -NoBackup -ErrorAction Stop | Out-Null
        return
    }

    if (Get-Command Update-MarkdownHelp -ErrorAction SilentlyContinue) {
        Update-MarkdownHelp -Path $MarkdownPath -Force -ErrorAction Stop | Out-Null
        return
    }

    throw "PlatyPS is not installed. Install Microsoft.PowerShell.PlatyPS (recommended) or legacy platyPS."
}

function Set-FlatMarkdownModuleMetadata {
    param(
        [Parameter(Mandatory)]
        [string]$MarkdownPath,

        [Parameter(Mandatory)]
        [string]$DevModuleName,

        [Parameter(Mandatory)]
        [string]$ProductionModuleName
    )

    $helpFileName = "$ProductionModuleName-help.xml"
    $content = Get-Content $MarkdownPath -Raw
    $content = $content -replace '(?m)^external help file:.*$', "external help file: $helpFileName"
    $content = $content -replace "Module Name: $DevModuleName", "Module Name: $ProductionModuleName"
    Set-Content -Path $MarkdownPath -Value $content -NoNewline
}

function Repair-PlatyPSMarkdownPlaceholders {
    param(
        [Parameter(Mandatory)]
        [string]$MarkdownPath
    )

    $content = Get-Content $MarkdownPath -Raw

    # PlatyPS 1.0 boilerplate sections with no module content.
    $content = $content -replace '(?ms)^## ALIASES\r?\n\r?\nThis cmdlet has the following aliases,\r?\n \{\{Insert list of aliases\}\}\r?\n\r?\n',''

    # Remove any standalone PlatyPS placeholder line.
    $content = $content -replace '(?m)^[ \t]*\{\{[^}\r\n]+\}\}\s*(?:\r?\n|$)', ''

    # Drop bare type headings left behind when PlatyPS invents pipeline input types.
    $content = $content -replace '(?ms)(?<=## INPUTS\r?\n\r?\n)^### (?:None\.|System\.[A-Za-z0-9_.\[\]]+)\r?\n\r?\n(?=### |## )',''

    # Normalize excessive blank lines introduced by removals.
    $content = $content -replace '(\r?\n){3,}', "`r`n`r`n"

    Set-Content -Path $MarkdownPath -Value $content -NoNewline
}

function Export-FlatExternalHelp {
    param(
        [Parameter(Mandatory)]
        [string]$DocumentationPath,

        [Parameter(Mandatory)]
        [string]$OutputPath,

        [Parameter(Mandatory)]
        [string]$ProductionModuleName
    )

    $helpFileName = "$ProductionModuleName-help.xml"
    $targetHelpPath = Join-Path $OutputPath $helpFileName

    if (Test-PlatyPSV1) {
        $markdownFiles = @(Measure-PlatyPSMarkdown -Path (Join-Path $DocumentationPath '*.md') -ErrorAction Stop)
        $commandMarkdownFiles = @($markdownFiles | Where-Object { $_.Filetype -match 'CommandHelp' })

        if ($commandMarkdownFiles.Count -eq 0) {
            throw "No command help markdown files found in '$DocumentationPath'."
        }

        $tempOutputPath = Join-Path ([System.IO.Path]::GetTempPath()) ("platyps-maml-$([Guid]::NewGuid().ToString())")
        New-Item -ItemType Directory -Path $tempOutputPath -Force | Out-Null

        try {
            $commandHelp = @(
                $commandMarkdownFiles |
                    Import-MarkdownCommandHelp -Path { $_.FilePath } |
                    ForEach-Object {
                        $_.ExternalHelpFile = $helpFileName
                        $_.ModuleName = $ProductionModuleName
                        $_
                    }
            )

            $commandHelp |
                Export-MamlCommandHelp -OutputFolder $tempOutputPath -Force -ErrorAction Stop | Out-Null

            $generatedHelpFiles = @(
                Get-ChildItem -Path $tempOutputPath -Recurse -Filter '*-help.xml' -File -ErrorAction SilentlyContinue
            )

            if ($generatedHelpFiles.Count -eq 0) {
                throw "No MAML help files were generated."
            }

            $mergedHelpFile = $generatedHelpFiles |
                Where-Object { $_.Name -ieq $helpFileName } |
                Sort-Object Length -Descending |
                Select-Object -First 1

            if (-not $mergedHelpFile) {
                throw "Expected MAML file '$helpFileName' was not generated."
            }

            New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
            Move-Item -Path $mergedHelpFile.FullName -Destination $targetHelpPath -Force
        }
        finally {
            Remove-Item -Path $tempOutputPath -Recurse -Force -ErrorAction SilentlyContinue
            Get-ChildItem -Path $OutputPath -Directory -ErrorAction SilentlyContinue |
                Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
        }

        return
    }

    if (Get-Command New-ExternalHelp -ErrorAction SilentlyContinue) {
        New-ExternalHelp -OutputPath $OutputPath -Path $DocumentationPath -Force -ErrorAction Stop | Out-Null
        return
    }

    throw "PlatyPS is not installed. Install Microsoft.PowerShell.PlatyPS (recommended) or legacy platyPS."
}

# Change to the repository directory
Push-Location $Path

try {
    if (-not (Get-Module Microsoft.PowerShell.PlatyPS -ListAvailable) -and -not (Get-Module PlatyPS -ListAvailable)) {
        throw "PlatyPS is not installed. Install Microsoft.PowerShell.PlatyPS with: Install-Module Microsoft.PowerShell.PlatyPS"
    }

    if (Get-Module Microsoft.PowerShell.PlatyPS -ListAvailable) {
        Import-Module Microsoft.PowerShell.PlatyPS -ErrorAction Stop
        Write-Host "Using Microsoft.PowerShell.PlatyPS $((Get-Module Microsoft.PowerShell.PlatyPS).Version)" -ForegroundColor Gray
    }
    else {
        Import-Module PlatyPS -ErrorAction Stop
        Write-Host "Using legacy platyPS $((Get-Module PlatyPS).Version)" -ForegroundColor Yellow
    }

    # Dynamically find the Dev module file (Dev.*.psd1)
    $devModule = Get-ChildItem -Path $PSScriptRoot -Filter "Dev.*.psd1" -File | Select-Object -First 1
    
    if (-not $devModule) {
        throw "No Dev module file (Dev.*.psd1) found in $PSScriptRoot"
    }
    
    Write-Host "Loading module: $($devModule.Name)" -ForegroundColor Cyan
    Import-Module $devModule.FullName -Force
    
    # Extract the production module name (remove "Dev." prefix)
    $productionModuleName = $devModule.BaseName -replace '^Dev\.', ''
    Write-Host "Production module name: $productionModuleName" -ForegroundColor Gray
}
catch {
    Write-Error "Failed to load Dev module: $_"
    Pop-Location
    return
}

try {
    if ($RepairAllDocumentation) {
        Write-Host "Repairing PlatyPS placeholders in all documentation files..." -ForegroundColor Cyan

        $documentationFiles = @(Get-ChildItem -Path $DocumentationPath -Filter '*.md' -File -ErrorAction Stop)
        foreach ($documentationFile in $documentationFiles) {
            Repair-PlatyPSMarkdownPlaceholders -MarkdownPath $documentationFile.FullName
        }

        Write-Host " ✓ Repaired $($documentationFiles.Count) documentation file(s)" -ForegroundColor Green

        Write-Host ""
        Write-Host "Regenerating external help files..." -ForegroundColor Cyan
        $enUSPath = Join-Path $PSScriptRoot "en-US"
        Export-FlatExternalHelp -DocumentationPath $DocumentationPath -OutputPath $enUSPath -ProductionModuleName $productionModuleName
        Write-Host " ✓ Successfully regenerated external help in $enUSPath" -ForegroundColor Green
        return
    }

    if ($ChangeSource -eq 'Unpushed') {
        $gitRange = Get-UnpushedGitRange
        Write-Host "Checking for cmdlet changes in unpushed commits ($gitRange)..." -ForegroundColor Cyan

        $changedPaths = @(git diff --name-only $gitRange)
        if ($LASTEXITCODE -ne 0) {
            throw "Failed to list changed files for git range '$gitRange'."
        }

        $modifiedCmdlets = Get-ModifiedPublicCmdletsFromGitPaths -ChangedPaths $changedPaths
        $noChangesMessage = "No modified cmdlet files found in unpushed commits."
    }
    else {
        Write-Host "Checking for modified PowerShell files in working tree..." -ForegroundColor Cyan

        $gitStatus = if ($IncludeStaged) {
            git status --porcelain
        }
        else {
            git status --porcelain | Where-Object { $_ -match '^\s*M' }
        }

        $changedPaths = $gitStatus | ForEach-Object {
            ($_ -split '\s+', 2)[-1]
        }

        $modifiedCmdlets = Get-ModifiedPublicCmdletsFromGitPaths -ChangedPaths $changedPaths
        $noChangesMessage = "No modified cmdlet files found in git status."
    }

    if (-not $modifiedCmdlets) {
        Write-Host $noChangesMessage -ForegroundColor Yellow
        return
    }
    
    Write-Host "Found $($modifiedCmdlets.Count) modified cmdlet(s):" -ForegroundColor Green
    $modifiedCmdlets | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }
    Write-Host ""
    
    # Update markdown documentation for each modified cmdlet
    $successCount = 0
    $failCount = 0
    $createCount = 0
    
    foreach ($cmdlet in $modifiedCmdlets) {
        $mdPath = Join-Path $DocumentationPath "$cmdlet.md"
        
        if (Test-Path $mdPath) {
            try {
                Write-Host "Updating $mdPath..." -ForegroundColor Cyan
                if ($RecreateUpdatedDocs) {
                    Remove-Item $mdPath -ErrorAction SilentlyContinue
                    New-FlatMarkdownCommandHelp -CommandName $cmdlet -OutputFolder $DocumentationPath -TargetPath $mdPath
                }
                else {
                    Update-FlatMarkdownCommandHelp -MarkdownPath $mdPath
                }
                
                Set-FlatMarkdownModuleMetadata -MarkdownPath $mdPath -DevModuleName $devModule.BaseName -ProductionModuleName $productionModuleName
                Repair-PlatyPSMarkdownPlaceholders -MarkdownPath $mdPath
                
                Write-Host " ✓ Successfully updated $cmdlet.md" -ForegroundColor Green
                $successCount++
            }
            catch {
                Write-Host " ✗ Failed to update $cmdlet.md: $_" -ForegroundColor Red
                $failCount++
            }
        }
        else {
            Write-Host "Creating $mdPath..." -ForegroundColor Cyan
            Try {
                New-FlatMarkdownCommandHelp -CommandName $cmdlet -OutputFolder $DocumentationPath -TargetPath $mdPath
                
                Set-FlatMarkdownModuleMetadata -MarkdownPath $mdPath -DevModuleName $devModule.BaseName -ProductionModuleName $productionModuleName
                Repair-PlatyPSMarkdownPlaceholders -MarkdownPath $mdPath
                    
                Write-Host " ✓ Successfully created $cmdlet.md" -ForegroundColor Green
                $createCount++ 
            }
            catch {
                Write-Host " ✗ Failed to create $cmdlet.md: $_" -ForegroundColor Red
                $failCount++
            }
        }
    }
    
    Write-Host ""
    Write-Host "Summary:" -ForegroundColor Cyan
    Write-Host " Updated: $successCount" -ForegroundColor Green
    Write-Host " Created: $createCount" -ForegroundColor Green
    if ($failCount -gt 0) {
        Write-Host " Failed/Missing: $failCount" -ForegroundColor Yellow
    }
    
    # Regenerate external help after all documentation updates
    if ($successCount -gt 0 -or $createCount -gt 0) {
        Write-Host ""
        Write-Host "Regenerating external help files..." -ForegroundColor Cyan
        try {
            $enUSPath = Join-Path $PSScriptRoot "en-US"
            Export-FlatExternalHelp -DocumentationPath $DocumentationPath -OutputPath $enUSPath -ProductionModuleName $productionModuleName
            Write-Host " ✓ Successfully regenerated external help in $enUSPath" -ForegroundColor Green
        }
        catch {
            Write-Host " ✗ Failed to regenerate external help: $_" -ForegroundColor Red
        }
    }
}
catch {
    Write-Error "An error occurred: $_"
}
finally {
    Pop-Location
}