scripts/specrew-update.ps1

param(
    [string]$ProjectPath = '.',
    [switch]$InfoMode,
    [switch]$All,
    [switch]$Specrew,
    [switch]$Squad,
    [switch]$SpecKit,
    [switch]$SkipUpdateCheck,
    [switch]$Help,
    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]]$CliArgs
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path (Split-Path -Parent $PSScriptRoot) 'extensions\specrew-speckit\scripts\shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

$versionCheckHelperPath = Join-Path $PSScriptRoot 'internal\version-check.ps1'
if (-not (Test-Path -LiteralPath $versionCheckHelperPath -PathType Leaf)) {
    throw "Missing version-check helper '$versionCheckHelperPath'."
}
. $versionCheckHelperPath

function Get-NativeExitCode {
    if (Get-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue) {
        return $global:LASTEXITCODE
    }

    return 0
}

function Convert-UnixStyleArguments {
    param(
        [string]$ProjectPath,
        [bool]$InfoMode,
        [bool]$All,
        [bool]$Specrew,
        [bool]$Squad,
        [bool]$SpecKit,
        [bool]$SkipUpdateCheck,
        [bool]$Help,
        [string[]]$CliArgs
    )

    $result = [ordered]@{
        ProjectPath = $ProjectPath
        InfoMode    = $InfoMode
        All         = $All
        Specrew     = $Specrew
        Squad       = $Squad
        SpecKit     = $SpecKit
        SkipUpdateCheck = $SkipUpdateCheck
        Help        = $Help
    }

    if (-not $CliArgs -or $CliArgs.Count -eq 0) {
        return [pscustomobject]$result
    }

    $index = 0
    while ($index -lt $CliArgs.Count) {
        $arg = $CliArgs[$index]
        switch ($arg) {
            '--project-path' {
                $index++
                if ($index -ge $CliArgs.Count) {
                    throw '--project-path requires a value.'
                }

                $result.ProjectPath = $CliArgs[$index]
            }
            '--info' {
                $result.InfoMode = $true
            }
            '--all' {
                $result.All = $true
            }
            '--specrew' {
                $result.Specrew = $true
            }
            '--squad' {
                $result.Squad = $true
            }
            '--spec-kit' {
                $result.SpecKit = $true
            }
            '--skip-update-check' {
                $result.SkipUpdateCheck = $true
            }
            '--help' {
                $result.Help = $true
            }
            default {
                throw ("Unknown argument '{0}'." -f $arg)
            }
        }

        $index++
    }

    return [pscustomobject]$result
}

function Show-Usage {
    @'
specrew update [options]
 
Options:
  -ProjectPath | --project-path <path>
                         Target Specrew-managed project directory (defaults to current directory)
  -InfoMode | --info Show current vs latest known versions without mutating the project
  -All | --all Update Specrew, Spec Kit, and Squad together
  -Specrew | --specrew Update Specrew-managed project surfaces only
  -Squad | --squad Upgrade Squad to the latest known compatible version
  -SpecKit | --spec-kit Upgrade Spec Kit to the latest known compatible version
  -SkipUpdateCheck | --skip-update-check
                         Skip the PSGallery latest-version check for this run
  -Help | --help Show usage
 
Behavior:
  - Bare `specrew update` refreshes Specrew-managed project assets only.
  - `specrew update --info` reports current and latest known versions for Specrew, Spec Kit, and Squad.
  - When Specrew-only update completes, the command still notifies you if newer Squad or Spec Kit versions are available.
'@
 | Write-Host
}

function Get-ParsedVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Value,

        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    $match = [regex]::Match($Value, '(?<version>\d+\.\d+\.\d+(?:\.\d+)?)')
    if (-not $match.Success) {
        throw "Could not parse $Name version from '$Value'."
    }

    return [version]$match.Groups['version'].Value
}

function Get-ExtensionVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath
    )

    $manifestContent = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8
    $versionMatch = [regex]::Match($manifestContent, '(?m)^\s*version:\s*"?(?<version>[^"\r\n]+)')
    if (-not $versionMatch.Success) {
        throw "Could not determine version from '$ManifestPath'."
    }

    return $versionMatch.Groups['version'].Value.Trim()
}

function Get-ConfigMap {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ConfigPath
    )

    $map = @{}
    if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) {
        return $map
    }

    foreach ($line in @(Get-Content -LiteralPath $ConfigPath -Encoding UTF8)) {
        $match = [regex]::Match($line, '^(?<key>[a-z_]+):\s*"?(?<value>[^"\r\n]*)"?\s*$')
        if ($match.Success) {
            $map[$match.Groups['key'].Value] = $match.Groups['value'].Value
        }
    }

    return $map
}

function Set-YamlScalarValue {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Content,

        [Parameter(Mandatory = $true)]
        [string]$Key,

        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $escapedKey = [regex]::Escape($Key)
    $replacement = '{0}: "{1}"' -f $Key, $Value.Replace('"', '\"')
    if ($Content -match ("(?m)^\s*{0}:\s*" -f $escapedKey)) {
        return [regex]::Replace($Content, "(?m)^\s*${escapedKey}:\s*.*$", $replacement)
    }

    $trimmed = $Content.TrimEnd()
    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        return $replacement + [Environment]::NewLine
    }

    return $trimmed + [Environment]::NewLine + $replacement + [Environment]::NewLine
}

function Update-SpecrewConfig {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ConfigPath,

        [AllowNull()]
        [string]$SpecrewVersion,

        [AllowNull()]
        [string]$SpecKitVersion,

        [AllowNull()]
        [string]$SquadVersion
    )

    $existingContent = if (Test-Path -LiteralPath $ConfigPath -PathType Leaf) {
        Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8
    }
    else {
        ''
    }

    $updatedContent = $existingContent
    $updatedContent = Set-YamlScalarValue -Content $updatedContent -Key 'schema' -Value 'v1'
    if (-not [string]::IsNullOrWhiteSpace($SpecrewVersion)) {
        $updatedContent = Set-YamlScalarValue -Content $updatedContent -Key 'specrew_version' -Value $SpecrewVersion
    }
    if (-not [string]::IsNullOrWhiteSpace($SpecKitVersion)) {
        $updatedContent = Set-YamlScalarValue -Content $updatedContent -Key 'speckit_version' -Value $SpecKitVersion
    }
    if (-not [string]::IsNullOrWhiteSpace($SquadVersion)) {
        $updatedContent = Set-YamlScalarValue -Content $updatedContent -Key 'squad_version' -Value $SquadVersion
    }

    if ($updatedContent -eq $existingContent) {
        return 'preserved'
    }

    [System.IO.File]::WriteAllText($ConfigPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
    return 'updated'
}

function Get-FirstNonEmptyOutputLine {
    param(
        [AllowEmptyCollection()]
        [string[]]$OutputLines
    )

    return @($OutputLines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1)[0]
}

function Get-VersionValidationResults {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ValidateScriptPath,

        [Parameter(Mandatory = $true)]
        [string]$MinimumSpecKitVersion,

        [Parameter(Mandatory = $true)]
        [string]$MinimumSquadVersion
    )

    return @(
        & $ValidateScriptPath `
            -MinimumSpecKitVersion $MinimumSpecKitVersion `
            -MinimumSquadVersion $MinimumSquadVersion `
            -PassThru
    )
}

function Get-OverrideVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    $value = [Environment]::GetEnvironmentVariable($Name, 'Process')
    if ([string]::IsNullOrWhiteSpace($value)) {
        return $null
    }

    return $value.Trim()
}

function Get-HighestSemanticVersion {
    param(
        [AllowEmptyCollection()]
        [string[]]$Candidates
    )

    $bestVersion = $null
    $bestText = $null
    foreach ($candidate in @($Candidates)) {
        if ([string]::IsNullOrWhiteSpace($candidate)) {
            continue
        }

        try {
            $parsed = Get-ParsedVersion -Value $candidate -Name 'candidate'
            if ($null -eq $bestVersion -or $parsed -gt $bestVersion) {
                $bestVersion = $parsed
                $bestText = $parsed.ToString()
            }
        }
        catch {
            continue
        }
    }

    return $bestText
}

function Get-LatestGitTagVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Repository
    )

    $output = @(& git ls-remote --tags --refs $Repository 2>&1)
    if ((Get-NativeExitCode) -ne 0) {
        return $null
    }

    $versions = foreach ($line in $output) {
        $match = [regex]::Match([string]$line, 'refs/tags/(?<tag>v?\d+\.\d+\.\d+(?:\.\d+)?)$')
        if ($match.Success) {
            $match.Groups['tag'].Value
        }
    }

    return Get-HighestSemanticVersion -Candidates $versions
}

function Get-LatestNpmPackageVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$PackageName
    )

    $output = @(& npm view $PackageName version 2>&1)
    if ((Get-NativeExitCode) -ne 0) {
        return $null
    }

    return Get-HighestSemanticVersion -Candidates $output
}

function Get-LatestVersionInfo {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Specrew', 'Spec Kit', 'Squad')]
        [string]$Platform,

        [Parameter(Mandatory = $true)]
        [string]$RepoRoot,

        [Parameter(Mandatory = $true)]
        [string]$SpecrewVersion
    )

    switch ($Platform) {
        'Specrew' {
            $override = Get-OverrideVersion -Name 'SPECREW_UPDATE_LATEST_SPECREW'
            if ($override) {
                return [pscustomobject]@{
                    Version = $override
                    Source  = 'override'
                    Known   = $true
                }
            }

            # Prefer the actually-installed module's manifest as the "latest known" version.
            # Git tags lag behind real shipping (we ship 0.22.0 without yet tagging v0.22.0),
            # so origin-tags drift falsely reports older versions. Module manifest is authoritative.
            $moduleVersion = $null
            try {
                $moduleVersion = Get-SpecrewInstalledVersion -ProjectRoot $RepoRoot
            }
            catch {
                $moduleVersion = $null
            }

            if ($moduleVersion) {
                return [pscustomobject]@{
                    Version = $moduleVersion
                    Source  = 'module-manifest'
                    Known   = $true
                }
            }

            # Fall back to origin-tags only when no module manifest is reachable.
            $remoteUrl = @(& git -C $RepoRoot remote get-url origin 2>$null)
            $remoteVersion = if ((Get-NativeExitCode) -eq 0 -and $remoteUrl) {
                Get-LatestGitTagVersion -Repository ([string]$remoteUrl[0])
            }
            else {
                $null
            }

            if ($remoteVersion) {
                return [pscustomobject]@{
                    Version = $remoteVersion
                    Source  = 'origin-tags'
                    Known   = $true
                }
            }

            return [pscustomobject]@{
                Version = $SpecrewVersion
                Source  = 'local-source'
                Known   = $true
            }
        }
        'Spec Kit' {
            $override = Get-OverrideVersion -Name 'SPECREW_UPDATE_LATEST_SPECKIT'
            if ($override) {
                return [pscustomobject]@{
                    Version = $override
                    Source  = 'override'
                    Known   = $true
                }
            }

            $latest = Get-LatestGitTagVersion -Repository 'https://github.com/github/spec-kit.git'
            return [pscustomobject]@{
                Version = $latest
                Source  = if ($latest) { 'github-tags' } else { 'unavailable' }
                Known   = -not [string]::IsNullOrWhiteSpace($latest)
            }
        }
        'Squad' {
            $override = Get-OverrideVersion -Name 'SPECREW_UPDATE_LATEST_SQUAD'
            if ($override) {
                return [pscustomobject]@{
                    Version = $override
                    Source  = 'override'
                    Known   = $true
                }
            }

            $latest = Get-LatestNpmPackageVersion -PackageName '@bradygaster/squad-cli'
            return [pscustomobject]@{
                Version = $latest
                Source  = if ($latest) { 'npm' } else { 'unavailable' }
                Known   = -not [string]::IsNullOrWhiteSpace($latest)
            }
        }
    }
}

function Get-SpecKitInstallArguments {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Version
    )

    $gitReference = if ($Version.StartsWith('v', [System.StringComparison]::OrdinalIgnoreCase)) {
        $Version
    }
    else {
        'v{0}' -f $Version
    }

    return @(
        'tool',
        'install',
        '--force',
        'specify-cli',
        '--from',
        ('git+https://github.com/github/spec-kit.git@{0}' -f $gitReference)
    )
}

function Install-PlatformVersion {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Spec Kit', 'Squad')]
        [string]$Platform,

        [Parameter(Mandatory = $true)]
        [string]$Version
    )

    switch ($Platform) {
        'Spec Kit' {
            if (-not (Get-Command -Name 'uv' -ErrorAction SilentlyContinue)) {
                throw "Cannot upgrade Spec Kit because 'uv' is unavailable."
            }

            & uv @(Get-SpecKitInstallArguments -Version $Version)
            if ((Get-NativeExitCode) -ne 0) {
                throw ("Failed to upgrade Spec Kit to {0}." -f $Version)
            }
        }
        'Squad' {
            if (-not (Get-Command -Name 'npm' -ErrorAction SilentlyContinue)) {
                throw "Cannot upgrade Squad because 'npm' is unavailable."
            }

            & npm install -g ("@bradygaster/squad-cli@{0}" -f $Version)
            if ((Get-NativeExitCode) -ne 0) {
                throw ("Failed to upgrade Squad to {0}." -f $Version)
            }
        }
    }
}

function Get-RequestedScopes {
    param(
        [bool]$All,
        [bool]$Specrew,
        [bool]$Squad,
        [bool]$SpecKit
    )

    $selectedSpecific = @()
    if ($Specrew) { $selectedSpecific += 'Specrew' }
    if ($Squad) { $selectedSpecific += 'Squad' }
    if ($SpecKit) { $selectedSpecific += 'Spec Kit' }

    if ($All -and $selectedSpecific.Count -gt 0) {
        throw 'Use either --all or explicit platform flags, not both.'
    }

    if ($All) {
        return @('Specrew', 'Spec Kit', 'Squad')
    }

    if ($selectedSpecific.Count -gt 0) {
        return $selectedSpecific
    }

    return @('Specrew')
}

function Compare-VersionState {
    param(
        [AllowNull()]
        [string]$CurrentVersion,

        [AllowNull()]
        [string]$LatestVersion
    )

    if ([string]::IsNullOrWhiteSpace($LatestVersion)) {
        return 'unknown'
    }

    if ([string]::IsNullOrWhiteSpace($CurrentVersion)) {
        return 'not-installed'
    }

    try {
        $current = Get-ParsedVersion -Value $CurrentVersion -Name 'current'
        $latest = Get-ParsedVersion -Value $LatestVersion -Name 'latest'
        if ($current -lt $latest) {
            return 'update-available'
        }

        if ($current -gt $latest) {
            return 'ahead-of-known'
        }

        return 'current'
    }
    catch {
        return 'unknown'
    }
}

function Get-TemplateRefreshMappings {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )

    return @(
        [pscustomobject]@{
            SourceRoot         = Join-Path -Path $RootPath -ChildPath '.specify\templates'
            TargetRelativeRoot = '.specify\templates'
            SourceLabelRoot    = '.specify/templates'
        }
        [pscustomobject]@{
            SourceRoot         = Join-Path -Path $RootPath -ChildPath '.squad\templates'
            TargetRelativeRoot = '.squad'
            SourceLabelRoot    = '.squad/templates'
        }
        [pscustomobject]@{
            SourceRoot         = Join-Path -Path $RootPath -ChildPath '.github\workflows'
            TargetRelativeRoot = '.github\workflows'
            SourceLabelRoot    = '.github/workflows'
        }
    )
}

function Get-TemplateInventory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath,

        [Parameter(Mandatory = $true)]
        [string]$ProjectPath
    )

    $inventory = @{}
    foreach ($mapping in @(Get-TemplateRefreshMappings -RootPath $RootPath)) {
        if (-not (Test-Path -LiteralPath $mapping.SourceRoot -PathType Container)) {
            continue
        }

        $files = @(Get-ChildItem -LiteralPath $mapping.SourceRoot -File -Recurse | Sort-Object FullName)
        foreach ($file in $files) {
            $relativeSourcePath = [System.IO.Path]::GetRelativePath($mapping.SourceRoot, $file.FullName)
            $projectRelativePath = Join-Path -Path $mapping.TargetRelativeRoot -ChildPath $relativeSourcePath
            $normalizedKey = $projectRelativePath.Replace('/', '\')
            $inventory[$normalizedKey] = [pscustomobject]@{
                SourcePath          = $file.FullName
                RelativeSourcePath  = $relativeSourcePath.Replace('\', '/')
                ProjectRelativePath = $normalizedKey
                TargetPath          = Join-Path -Path $ProjectPath -ChildPath $projectRelativePath
                SourceTemplatePath  = '{0}/{1}' -f $mapping.SourceLabelRoot.TrimEnd('/'), $relativeSourcePath.Replace('\', '/')
            }
        }
    }

    return $inventory
}

function Get-NullableFileContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return $null
    }

    return Get-Content -LiteralPath $Path -Raw -Encoding UTF8
}

function Get-ContentHash {
    param(
        [AllowNull()]
        [string]$Content
    )

    if ($null -eq $Content) {
        return $null
    }

    $sha256 = [System.Security.Cryptography.SHA256]::Create()
    try {
        $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Content)
        return ([System.BitConverter]::ToString($sha256.ComputeHash($bytes))).Replace('-', '')
    }
    finally {
        $sha256.Dispose()
    }
}

function Resolve-PreviousSpecrewRoot {
    param(
        [Parameter(Mandatory = $true)]
        [string]$CurrentRoot,

        [AllowNull()]
        [string]$CurrentVersion,

        [AllowNull()]
        [string]$ProjectVersion
    )

    if ([string]::IsNullOrWhiteSpace($ProjectVersion)) {
        return $null
    }

    if ($ProjectVersion -eq $CurrentVersion) {
        return $CurrentRoot
    }

    $parentRoot = Split-Path -Parent $CurrentRoot
    if ([string]::IsNullOrWhiteSpace($parentRoot)) {
        return $null
    }

    $candidateRoot = Join-Path -Path $parentRoot -ChildPath $ProjectVersion
    $candidateUpdateScript = Join-Path -Path $candidateRoot -ChildPath 'scripts\specrew-update.ps1'
    if ((Test-Path -LiteralPath $candidateRoot -PathType Container) -and (Test-Path -LiteralPath $candidateUpdateScript -PathType Leaf)) {
        return $candidateRoot
    }

    return $null
}

function Get-TemplateArtifactBaseName {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRelativePath
    )

    $trimmed = $ProjectRelativePath.TrimStart('.', '\', '/')
    $safeName = $trimmed -replace '[\\/:*?"<>|]+', '__'
    $safeName = $safeName.Trim('_')
    if ([string]::IsNullOrWhiteSpace($safeName)) {
        return 'template-refresh'
    }

    return $safeName
}

function Ensure-ParentDirectory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $parent = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent -PathType Container)) {
        $null = New-Item -ItemType Directory -Path $parent -Force
    }
}

function Format-ConflictArtifactContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserContent,

        [Parameter(Mandatory = $true)]
        [string]$ModuleContent,

        [Parameter(Mandatory = $true)]
        [string]$PreservedAt,

        [Parameter(Mandatory = $true)]
        [string]$ModuleVersion,

        [Parameter(Mandatory = $true)]
        [string]$SourceTemplatePath
    )

    $builder = [System.Text.StringBuilder]::new()
    $null = $builder.Append('<<<<<<< user-version (preserved at: ').Append($PreservedAt).Append(')').Append([Environment]::NewLine)
    $null = $builder.Append($UserContent)
    if (-not $UserContent.EndsWith("`n")) {
        $null = $builder.Append([Environment]::NewLine)
    }

    $null = $builder.Append('=======').Append([Environment]::NewLine)
    $null = $builder.Append($ModuleContent)
    if (-not $ModuleContent.EndsWith("`n")) {
        $null = $builder.Append([Environment]::NewLine)
    }

    $null = $builder.Append('>>>>>>> module-version (specrew_version: ').Append($ModuleVersion).Append(', source: ').Append($SourceTemplatePath).Append(')').Append([Environment]::NewLine)
    return $builder.ToString()
}

function Format-DeletionArtifactContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectRelativePath,

        [Parameter(Mandatory = $true)]
        [string]$PreservedAt,

        [Parameter(Mandatory = $true)]
        [string]$PreviousVersion,

        [Parameter(Mandatory = $true)]
        [string]$CurrentVersion,

        [Parameter(Mandatory = $true)]
        [string]$SourceTemplatePath
    )

    return @(
        '# Specrew template deletion review'
        ''
        ('preserved_at_utc: {0}' -f $PreservedAt)
        ('template_path: {0}' -f $ProjectRelativePath)
        ('previous_specrew_version: {0}' -f $PreviousVersion)
        ('current_specrew_version: {0}' -f $CurrentVersion)
        ('previous_source: {0}' -f $SourceTemplatePath)
        'resolution: pending-manual-review'
        ''
        'The current Specrew module no longer ships this template.'
        'Review the preserved project file and decide whether to keep it, archive it, or remove it manually.'
    ) -join [Environment]::NewLine
}

function Get-TemplateChangeClassification {
    param(
        [AllowNull()]
        [string]$BaselineContent,

        [AllowNull()]
        [string]$ProjectContent,

        [AllowNull()]
        [string]$CurrentContent,

        [Parameter(Mandatory = $true)]
        [string]$CurrentVersion,

        [AllowNull()]
        [string]$ProjectVersion
    )

    if ($null -eq $CurrentContent) {
        return 'absent'
    }

    if ($null -eq $ProjectContent) {
        if ($null -eq $BaselineContent) {
            return 'new-template'
        }

        return 'module-only'
    }

    if ($null -eq $BaselineContent) {
        if ($ProjectContent -eq $CurrentContent) {
            return 'no-change'
        }

        if ($CurrentVersion -eq $ProjectVersion) {
            return 'user-only'
        }

        return 'both-modified'
    }

    $userChanged = ($ProjectContent -ne $BaselineContent)
    $moduleChanged = ($CurrentContent -ne $BaselineContent)

    if (-not $userChanged -and -not $moduleChanged) {
        return 'no-change'
    }
    if ($userChanged -and -not $moduleChanged) {
        return 'user-only'
    }
    if (-not $userChanged -and $moduleChanged) {
        return 'module-only'
    }

    return 'both-modified'
}

function Invoke-TemplateRefresh {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProjectPath,

        [Parameter(Mandatory = $true)]
        [string]$CurrentRoot,

        [AllowNull()]
        [string]$PreviousRoot,

        [Parameter(Mandatory = $true)]
        [string]$CurrentVersion,

        [AllowNull()]
        [string]$ProjectVersion
    )

    $actions = [System.Collections.ArrayList]::new()
    $artifactRoot = Join-Path -Path $ProjectPath -ChildPath '.specrew\template-conflicts'
    $currentInventory = Get-TemplateInventory -RootPath $CurrentRoot -ProjectPath $ProjectPath
    $previousInventory = if ($PreviousRoot) {
        Get-TemplateInventory -RootPath $PreviousRoot -ProjectPath $ProjectPath
    }
    else {
        @{}
    }

    if ($PreviousRoot -or $currentVersion -eq $ProjectVersion) {
        foreach ($projectRelativePath in @($currentInventory.Keys | Sort-Object)) {
            $currentTemplate = $currentInventory[$projectRelativePath]
            $previousTemplate = if ($previousInventory.ContainsKey($projectRelativePath)) { $previousInventory[$projectRelativePath] } else { $null }
            $baselineContent = if ($null -ne $previousTemplate) { Get-NullableFileContent -Path $previousTemplate.SourcePath } else { $null }
            $projectContent = Get-NullableFileContent -Path $currentTemplate.TargetPath
            $currentContent = Get-NullableFileContent -Path $currentTemplate.SourcePath
            $classification = Get-TemplateChangeClassification `
                -BaselineContent $baselineContent `
                -ProjectContent $projectContent `
                -CurrentContent $currentContent `
                -CurrentVersion $CurrentVersion `
                -ProjectVersion $ProjectVersion

            switch ($classification) {
                'module-only' {
                    Ensure-ParentDirectory -Path $currentTemplate.TargetPath
                    Write-Utf8FileAtomic -Path $currentTemplate.TargetPath -Content $currentContent
                    $null = $actions.Add([pscustomobject]@{
                            Action   = 'template-updated'
                            Detail   = $currentTemplate.ProjectRelativePath
                            Template = $currentTemplate.ProjectRelativePath
                        })
                }
                'new-template' {
                    Ensure-ParentDirectory -Path $currentTemplate.TargetPath
                    Write-Utf8FileAtomic -Path $currentTemplate.TargetPath -Content $currentContent
                    $null = $actions.Add([pscustomobject]@{
                            Action   = 'template-added'
                            Detail   = $currentTemplate.ProjectRelativePath
                            Template = $currentTemplate.ProjectRelativePath
                        })
                }
                'both-modified' {
                    $preservedAt = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
                    $artifactBaseName = Get-TemplateArtifactBaseName -ProjectRelativePath $currentTemplate.ProjectRelativePath
                    $artifactPath = Join-Path -Path $artifactRoot -ChildPath ('{0}.conflict' -f $artifactBaseName)
                    $conflictContent = Format-ConflictArtifactContent `
                        -UserContent $projectContent `
                        -ModuleContent $currentContent `
                        -PreservedAt $preservedAt `
                        -ModuleVersion $CurrentVersion `
                        -SourceTemplatePath $currentTemplate.SourceTemplatePath

                    Write-Utf8FileAtomic -Path $artifactPath -Content $conflictContent
                    Write-Utf8FileAtomic -Path $currentTemplate.TargetPath -Content $conflictContent
                    $null = $actions.Add([pscustomobject]@{
                            Action   = 'template-conflict'
                            Detail   = ('{0} -> {1}' -f $currentTemplate.ProjectRelativePath, $artifactPath)
                            Template = $currentTemplate.ProjectRelativePath
                        })
                }
            }
        }

        foreach ($projectRelativePath in @($previousInventory.Keys | Sort-Object)) {
            if ($currentInventory.ContainsKey($projectRelativePath)) {
                continue
            }

            $previousTemplate = $previousInventory[$projectRelativePath]
            if (-not (Test-Path -LiteralPath $previousTemplate.TargetPath -PathType Leaf)) {
                continue
            }

            $preservedAt = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
            $artifactBaseName = Get-TemplateArtifactBaseName -ProjectRelativePath $projectRelativePath
            $artifactPath = Join-Path -Path $artifactRoot -ChildPath ('{0}.deletion' -f $artifactBaseName)
            $artifactContent = Format-DeletionArtifactContent `
                -ProjectRelativePath $projectRelativePath `
                -PreservedAt $preservedAt `
                -PreviousVersion $(if ($ProjectVersion) { $ProjectVersion } else { 'unknown' }) `
                -CurrentVersion $CurrentVersion `
                -SourceTemplatePath $previousTemplate.SourceTemplatePath

            Write-Utf8FileAtomic -Path $artifactPath -Content $artifactContent
            $null = $actions.Add([pscustomobject]@{
                    Action   = 'template-deleted'
                    Detail   = ('{0} -> {1}' -f $projectRelativePath, $artifactPath)
                    Template = $projectRelativePath
                })
        }
    }
    else {
        $null = $actions.Add([pscustomobject]@{
                Action   = 'template-baseline-unavailable'
                Detail   = ('Could not locate module version {0}; template refresh fell back to managed asset updates only.' -f $ProjectVersion)
                Template = $null
            })
    }

    return $actions
}

$parsedArgs = Convert-UnixStyleArguments `
    -ProjectPath $ProjectPath `
    -InfoMode $InfoMode.IsPresent `
    -All $All.IsPresent `
    -Specrew $Specrew.IsPresent `
    -Squad $Squad.IsPresent `
    -SpecKit $SpecKit.IsPresent `
    -SkipUpdateCheck $SkipUpdateCheck.IsPresent `
    -Help $Help.IsPresent `
    -CliArgs $CliArgs

$ProjectPath = $parsedArgs.ProjectPath
$InfoMode = [bool]$parsedArgs.InfoMode
$All = [bool]$parsedArgs.All
$Specrew = [bool]$parsedArgs.Specrew
$Squad = [bool]$parsedArgs.Squad
$SpecKit = [bool]$parsedArgs.SpecKit
$SkipUpdateCheck = [bool]$parsedArgs.SkipUpdateCheck
$Help = [bool]$parsedArgs.Help

if ($Help) {
    Show-Usage
    exit 0
}

$resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath
$repoRoot = Split-Path -Parent $PSScriptRoot
$configPath = Join-Path $resolvedProjectPath '.specrew\config.yml'
$specrewManifestPath = Join-Path $repoRoot 'extensions\specrew-speckit\extension.yml'
$validateVersionsScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\validate-versions.ps1'
$deploySpeckitExtensionScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\deploy-speckit-extension.ps1'
$deploySquadRuntimeScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\deploy-squad-runtime.ps1'
$minimumSpecKitVersion = '0.8.4'
$minimumSquadVersion = '0.9.1'

foreach ($requiredPath in @($specrewManifestPath, $validateVersionsScript, $deploySpeckitExtensionScript, $deploySquadRuntimeScript)) {
    if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
        Write-Error ("Required helper is missing: {0}" -f $requiredPath)
        exit 1
    }
}

if (-not (Test-Path -LiteralPath $resolvedProjectPath -PathType Container)) {
    Write-Error ("Project path does not exist: {0}" -f $resolvedProjectPath)
    exit 1
}

if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
    Write-Error ("Project is not Specrew-managed. Missing '{0}'." -f $configPath)
    exit 1
}

$scopes = @()
try {
    $scopes = @(Get-RequestedScopes -All $All -Specrew $Specrew -Squad $Squad -SpecKit $SpecKit)
}
catch {
    Write-Error $_.Exception.Message
    exit 1
}

$projectConfig = Get-ConfigMap -ConfigPath $configPath
$sourceSpecrewVersion = Get-ExtensionVersion -ManifestPath $specrewManifestPath
$deployedSpecrewManifestPath = Join-Path $resolvedProjectPath '.specify\extensions\specrew-speckit\extension.yml'
$currentSpecrewVersion = if (Test-Path -LiteralPath $deployedSpecrewManifestPath -PathType Leaf) {
    Get-ExtensionVersion -ManifestPath $deployedSpecrewManifestPath
}
elseif ($projectConfig.ContainsKey('specrew_version')) {
    [string]$projectConfig['specrew_version']
}
else {
    $null
}

$validationResults = @()
try {
    $validationResults = @(Get-VersionValidationResults -ValidateScriptPath $validateVersionsScript -MinimumSpecKitVersion $minimumSpecKitVersion -MinimumSquadVersion $minimumSquadVersion)
}
catch {
    Write-Error ("Failed to probe installed Spec Kit / Squad versions. {0}" -f $_.Exception.Message)
    exit 1
}

$validationByPlatform = @{}
foreach ($result in $validationResults) {
    $validationByPlatform[$result.Platform] = $result
}

$latestByPlatform = @{
    'Specrew'  = Get-LatestVersionInfo -Platform 'Specrew' -RepoRoot $repoRoot -SpecrewVersion $sourceSpecrewVersion
    'Spec Kit' = Get-LatestVersionInfo -Platform 'Spec Kit' -RepoRoot $repoRoot -SpecrewVersion $sourceSpecrewVersion
    'Squad'    = Get-LatestVersionInfo -Platform 'Squad' -RepoRoot $repoRoot -SpecrewVersion $sourceSpecrewVersion
}

$infoRows = @(
    [pscustomobject]@{
        Platform    = 'Specrew'
        Current     = if ($currentSpecrewVersion) { $currentSpecrewVersion } else { 'not-recorded' }
        LatestKnown = if ($latestByPlatform['Specrew'].Known) { $latestByPlatform['Specrew'].Version } else { 'unavailable' }
        Status      = Compare-VersionState -CurrentVersion $currentSpecrewVersion -LatestVersion $latestByPlatform['Specrew'].Version
        Source      = $latestByPlatform['Specrew'].Source
    }
    [pscustomobject]@{
        Platform    = 'Spec Kit'
        Current     = if ($validationByPlatform.ContainsKey('Spec Kit') -and $validationByPlatform['Spec Kit'].Version) { $validationByPlatform['Spec Kit'].Version } elseif ($projectConfig.ContainsKey('speckit_version')) { [string]$projectConfig['speckit_version'] } else { 'not-installed' }
        LatestKnown = if ($latestByPlatform['Spec Kit'].Known) { $latestByPlatform['Spec Kit'].Version } else { 'unavailable' }
        Status      = Compare-VersionState -CurrentVersion $(if ($validationByPlatform.ContainsKey('Spec Kit')) { $validationByPlatform['Spec Kit'].Version } else { $null }) -LatestVersion $latestByPlatform['Spec Kit'].Version
        Source      = $latestByPlatform['Spec Kit'].Source
    }
    [pscustomobject]@{
        Platform    = 'Squad'
        Current     = if ($validationByPlatform.ContainsKey('Squad') -and $validationByPlatform['Squad'].Version) { $validationByPlatform['Squad'].Version } elseif ($projectConfig.ContainsKey('squad_version')) { [string]$projectConfig['squad_version'] } else { 'not-installed' }
        LatestKnown = if ($latestByPlatform['Squad'].Known) { $latestByPlatform['Squad'].Version } else { 'unavailable' }
        Status      = Compare-VersionState -CurrentVersion $(if ($validationByPlatform.ContainsKey('Squad')) { $validationByPlatform['Squad'].Version } else { $null }) -LatestVersion $latestByPlatform['Squad'].Version
        Source      = $latestByPlatform['Squad'].Source
    }
)

if ($InfoMode) {
    Write-Host ("Version info for {0}" -f $resolvedProjectPath) -ForegroundColor Green
    $infoRows | Format-Table -AutoSize
    exit 0
}

$summary = [System.Collections.ArrayList]::new()
$installFailureMessage = $null
$previousSpecrewRoot = Resolve-PreviousSpecrewRoot `
    -CurrentRoot $repoRoot `
    -CurrentVersion $sourceSpecrewVersion `
    -ProjectVersion $currentSpecrewVersion

if ($scopes -contains 'Specrew') {
    $versionTransition = if ([string]::IsNullOrWhiteSpace($currentSpecrewVersion)) {
        'not-recorded -> {0}' -f $sourceSpecrewVersion
    }
    else {
        '{0} -> {1}' -f $currentSpecrewVersion, $sourceSpecrewVersion
    }

    $null = $summary.Add([pscustomobject]@{
            Platform = 'Specrew'
            Action   = 'module-version-detected'
            Detail   = $versionTransition
        })
}

if ($scopes -contains 'Specrew') {
    if (Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.specify') -PathType Container) {
        $specKitDeploymentActions = @(
            & $deploySpeckitExtensionScript `
                -ProjectPath $resolvedProjectPath `
                -RefreshExisting `
                -PassThru
        )

        foreach ($action in $specKitDeploymentActions) {
            $null = $summary.Add([pscustomobject]@{
                    Platform = 'Specrew'
                    Action   = [string]$action.Action
                    Detail   = [string]$action.Path
                })
        }
    }
    else {
        $null = $summary.Add([pscustomobject]@{
                Platform = 'Specrew'
                Action   = 'skipped'
                Detail   = '.specify is absent in this project'
            })
    }

    if (Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.squad') -PathType Container) {
        $squadDeploymentActions = @(
            & $deploySquadRuntimeScript `
                -ProjectPath $resolvedProjectPath `
                -PassThru
        )

        foreach ($action in $squadDeploymentActions) {
            $null = $summary.Add([pscustomobject]@{
                    Platform = 'Specrew'
                    Action   = [string]$action.Action
                    Detail   = [string]$action.Path
                })
        }

        $null = $summary.Add([pscustomobject]@{
                Platform = 'Specrew'
                Action   = 'slash-surface-refreshed'
                Detail   = '/specrew-where, /specrew-status, /specrew-update, /specrew-team, /specrew-review, /specrew-help, /specrew-version across .claude/skills, .github/skills, and .agents/skills'
            })
    }
    else {
        $null = $summary.Add([pscustomobject]@{
                Platform = 'Specrew'
                Action   = 'skipped'
                Detail   = '.squad is absent in this project'
            })
    }

    $templateRefreshActions = @(
        Invoke-TemplateRefresh `
            -ProjectPath $resolvedProjectPath `
            -CurrentRoot $repoRoot `
            -PreviousRoot $previousSpecrewRoot `
            -CurrentVersion $sourceSpecrewVersion `
            -ProjectVersion $currentSpecrewVersion
    )

    foreach ($action in $templateRefreshActions) {
        $null = $summary.Add([pscustomobject]@{
                Platform = 'Specrew'
                Action   = [string]$action.Action
                Detail   = [string]$action.Detail
            })
    }
}

foreach ($platform in @('Spec Kit', 'Squad')) {
    if ($scopes -notcontains $platform) {
        continue
    }

    $latestInfo = $latestByPlatform[$platform]
    if (-not $latestInfo.Known) {
        Write-Error ("Cannot update {0} because the latest known version could not be determined." -f $platform)
        exit 1
    }

    $currentVersion = if ($validationByPlatform.ContainsKey($platform)) {
        [string]$validationByPlatform[$platform].Version
    }
    else {
        $null
    }

    $state = Compare-VersionState -CurrentVersion $currentVersion -LatestVersion $latestInfo.Version
    if ($state -eq 'current') {
        $null = $summary.Add([pscustomobject]@{
                Platform = $platform
                Action   = 'already-current'
                Detail   = $latestInfo.Version
            })
        continue
    }

    try {
        Install-PlatformVersion -Platform $platform -Version $latestInfo.Version
        $null = $summary.Add([pscustomobject]@{
                Platform = $platform
                Action   = 'upgraded'
                Detail   = $latestInfo.Version
            })
    }
    catch {
        $installFailureMessage = $_.Exception.Message
        $null = $summary.Add([pscustomobject]@{
                Platform = $platform
                Action   = 'failed'
                Detail   = $installFailureMessage
            })
        break
    }
}

$postValidationResults = @()
try {
    $postValidationResults = @(Get-VersionValidationResults -ValidateScriptPath $validateVersionsScript -MinimumSpecKitVersion $minimumSpecKitVersion -MinimumSquadVersion $minimumSquadVersion)
}
catch {
    Write-Error ("Failed to refresh recorded versions after update. {0}" -f $_.Exception.Message)
    exit 1
}

$postValidationByPlatform = @{}
foreach ($result in $postValidationResults) {
    $postValidationByPlatform[$result.Platform] = $result
}

$configAction = Update-SpecrewConfig `
    -ConfigPath $configPath `
    -SpecrewVersion $(if ($scopes -contains 'Specrew') { $sourceSpecrewVersion } else { $null }) `
    -SpecKitVersion $(if ($postValidationByPlatform.ContainsKey('Spec Kit')) { [string]$postValidationByPlatform['Spec Kit'].Version } elseif ($projectConfig.ContainsKey('speckit_version')) { [string]$projectConfig['speckit_version'] } else { $null }) `
    -SquadVersion $(if ($postValidationByPlatform.ContainsKey('Squad')) { [string]$postValidationByPlatform['Squad'].Version } elseif ($projectConfig.ContainsKey('squad_version')) { [string]$projectConfig['squad_version'] } else { $null })

$null = $summary.Add([pscustomobject]@{
        Platform = 'Specrew'
        Action   = $configAction
        Detail   = $configPath
    })

Write-Host ("Update summary for {0}" -f $resolvedProjectPath) -ForegroundColor Green
$summary | Format-Table -AutoSize

$otherUpdates = @($infoRows | Where-Object { $scopes -notcontains $_.Platform -and $_.Status -eq 'update-available' })
if ($otherUpdates.Count -gt 0) {
    Write-Host ''
    Write-Host 'Additional platform updates are available:' -ForegroundColor Yellow
    $otherUpdates | Select-Object Platform, Current, LatestKnown | Format-Table -AutoSize
}

$psGalleryUpdateWarning = Get-PSGalleryUpdateWarning -ProjectRoot $resolvedProjectPath -SkipCheck:$SkipUpdateCheck
if (-not [string]::IsNullOrWhiteSpace($psGalleryUpdateWarning)) {
    Write-Output ("WARN: {0}" -f $psGalleryUpdateWarning)
}

if ($installFailureMessage) {
    Write-Error $installFailureMessage
    exit 1
}

exit 0