scripts/specrew-where.ps1

[CmdletBinding()]
param(
    [string]$ProjectPath = '.',
    [string]$FeatureId,
    [string]$IterationNumber,
    [switch]$Compact,
    [switch]$Ascii,
    [switch]$NoColor,
    [int]$RecentCount = 6,
    [int]$BarWidth = 28,
    [switch]$Json,
    [switch]$Team,
    [switch]$Worktrees,
    [switch]$Help,
    [string]$OutputPath,
    [ValidateSet('live', 'iteration-closeout', 'feature-closeout')]
    [string]$CaptureKind = 'live',
    [switch]$PreserveExistingArtifact,
    [AllowEmptyString()][string]$HistoricalNotice,
    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]]$CliArgs
)

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

function Test-SpecrewWhereArgumentPresent {
    param(
        [string[]]$ArgumentList,
        [string[]]$OptionNames
    )

    foreach ($argument in @($ArgumentList | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) {
        foreach ($optionName in $OptionNames) {
            if ($argument -eq $optionName -or $argument.StartsWith(('{0}=' -f $optionName), [System.StringComparison]::OrdinalIgnoreCase)) {
                return $true
            }
        }
    }

    return $false
}

function Get-SpecrewWhereRawCaptureKind {
    param([string[]]$ArgumentList)

    $normalizedArguments = @($ArgumentList | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    for ($index = 0; $index -lt $normalizedArguments.Count; $index++) {
        $argument = $normalizedArguments[$index]
        if ($argument -match '^--capture-kind=(.+)$') {
            return $Matches[1]
        }

        if ($argument -ieq '--capture-kind') {
            $index++
            if ($index -lt $normalizedArguments.Count) {
                return $normalizedArguments[$index]
            }
        }
    }

    return $null
}

function Test-SpecrewShouldPrimeUtf8 {
    param(
        [bool]$Ascii,
        [bool]$NoColor,
        [bool]$Json,
        [bool]$Help,
        [string]$CaptureKind,
        [string[]]$CliArgs
    )

    if ($Ascii -or $NoColor -or $Json -or $Help) {
        return $false
    }

    if (-not [string]::IsNullOrWhiteSpace($env:NO_COLOR) -or -not [string]::IsNullOrWhiteSpace($env:NO_UNICODE)) {
        return $false
    }

    if (Test-SpecrewWhereArgumentPresent -ArgumentList $CliArgs -OptionNames @('--ascii', '--no-color', '--json', '--help', '-h')) {
        return $false
    }

    $rawCaptureKind = if (-not [string]::IsNullOrWhiteSpace($CaptureKind)) { $CaptureKind } else { Get-SpecrewWhereRawCaptureKind -ArgumentList $CliArgs }
    if (-not [string]::IsNullOrWhiteSpace($rawCaptureKind) -and $rawCaptureKind -ine 'live') {
        return $false
    }

    return $true
}

function Enter-SpecrewUtf8ConsoleScope {
    $utf8Encoding = [System.Text.UTF8Encoding]::new($false)
    $state = [pscustomobject]@{
        InputEncoding  = $null
        OutputEncoding = $null
        VariableValue  = $null
        Changed        = $false
    }

    try {
        $state.InputEncoding = [Console]::InputEncoding
    }
    catch {
        $state.InputEncoding = $null
    }

    try {
        $state.OutputEncoding = [Console]::OutputEncoding
    }
    catch {
        $state.OutputEncoding = $null
    }

    try {
        $state.VariableValue = (Get-Variable -Name OutputEncoding -Scope Global -ErrorAction Stop).Value
    }
    catch {
        $state.VariableValue = $null
    }

    try {
        if ($null -eq $state.InputEncoding -or $state.InputEncoding.WebName -notmatch 'utf-?8') {
            [Console]::InputEncoding = $utf8Encoding
            $state.Changed = $true
        }

        if ($null -eq $state.OutputEncoding -or $state.OutputEncoding.WebName -notmatch 'utf-?8') {
            [Console]::OutputEncoding = $utf8Encoding
            $state.Changed = $true
        }

        if ($null -eq $state.VariableValue -or $state.VariableValue.WebName -notmatch 'utf-?8') {
            Set-Variable -Name OutputEncoding -Scope Global -Value $utf8Encoding
            $state.Changed = $true
        }
    }
    catch {
        # Best-effort only; rendering still falls back truthfully if UTF-8 remains unavailable.
    }

    return $state
}

function Exit-SpecrewUtf8ConsoleScope {
    param([AllowNull()][object]$State)

    if ($null -eq $State -or -not $State.Changed) {
        return
    }

    try {
        if ($null -ne $State.InputEncoding) {
            [Console]::InputEncoding = $State.InputEncoding
        }
    }
    catch {
    }

    try {
        if ($null -ne $State.OutputEncoding) {
            [Console]::OutputEncoding = $State.OutputEncoding
        }
    }
    catch {
    }

    try {
        if ($null -ne $State.VariableValue) {
            Set-Variable -Name OutputEncoding -Scope Global -Value $State.VariableValue
        }
    }
    catch {
    }
}

$utf8ConsoleScope = $null
if (Test-SpecrewShouldPrimeUtf8 `
        -Ascii $Ascii.IsPresent `
        -NoColor $NoColor.IsPresent `
        -Json $Json.IsPresent `
        -Help $Help.IsPresent `
        -CaptureKind $CaptureKind `
        -CliArgs $CliArgs) {
    $utf8ConsoleScope = Enter-SpecrewUtf8ConsoleScope
}

$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

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

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

function Show-Usage {
    @'
specrew where - show the velocity dashboard ("where am I?")
 
Usage:
  specrew where [options]
  specrew status [options]
  scripts/specrew-where.ps1 [options]
 
Options:
  --project-path <path> Target project root (default: current directory)
  --feature <id> Restrict the dashboard to one feature
  --iteration <NNN> Focus on one iteration when it exists
  --compact Render the fixed compact dashboard (24 lines max)
  --ASCII Force monochrome / ASCII-safe fallback rendering
  --no-color Force monochrome output
  --RecentCount <N> Show N Recent Shipped entries (default: 6)
  --BarWidth <N> Use N columns for rich shipped bars (default: 28)
  --team Reserved team path; falls back to the personal dashboard
  --worktrees List all git worktrees with feature and boundary state
  --json Emit the assembled snapshot as JSON
  --output-path <path> Persist the rendered dashboard or closeout snapshot
  --capture-kind <kind> live | iteration-closeout | feature-closeout
  --preserve-existing-artifact
                         When writing an artifact, keep any existing file untouched
  --help Show this help message
 
Examples:
  specrew where
  specrew status --compact
  specrew where --ASCII
  specrew where --RecentCount 4 --BarWidth 20
  specrew where --no-color
  specrew where --team
  pwsh -NoProfile -File .\scripts\specrew-where.ps1 --ASCII --BarWidth 20
'@
 | Write-Host
}

function Convert-UnixStyleArguments {
    param(
        [string]$ProjectPath,
        [string]$FeatureId,
        [string]$IterationNumber,
        [bool]$Compact,
        [bool]$Ascii,
        [bool]$NoColor,
        [int]$RecentCount,
        [int]$BarWidth,
        [bool]$Json,
        [bool]$Team,
        [bool]$Worktrees,
        [bool]$Help,
        [string]$OutputPath,
        [string]$CaptureKind,
        [bool]$PreserveExistingArtifact,
        [string]$HistoricalNotice,
        [string[]]$CliArgs
    )

    $result = [ordered]@{
        ProjectPath              = $ProjectPath
        FeatureId                = $FeatureId
        IterationNumber          = $IterationNumber
        Compact                  = $Compact
        Ascii                    = $Ascii
        NoColor                  = $NoColor
        RecentCount              = if ($RecentCount -gt 0) { $RecentCount } else { 6 }
        BarWidth                 = if ($BarWidth -gt 0) { $BarWidth } else { 28 }
        Json                     = $Json
        Team                     = $Team
        Worktrees                = $Worktrees
        Help                     = $Help
        OutputPath               = $OutputPath
        CaptureKind              = $CaptureKind
        PreserveExistingArtifact = $PreserveExistingArtifact
        HistoricalNotice         = $HistoricalNotice
    }

    $CliArgs = @($CliArgs | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    for ($index = 0; $index -lt $CliArgs.Count; $index++) {
        $argument = $CliArgs[$index]
        $parsedValue = 0
        switch -Regex ($argument) {
            '^--project-path(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.ProjectPath = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--project-path requires a value.' }
                    $result.ProjectPath = $CliArgs[$index]
                }
            }
            '^--feature(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.FeatureId = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--feature requires a value.' }
                    $result.FeatureId = $CliArgs[$index]
                }
            }
            '^--iteration(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.IterationNumber = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--iteration requires a value.' }
                    $result.IterationNumber = $CliArgs[$index]
                }
            }
            '^--output-path(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.OutputPath = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--output-path requires a value.' }
                    $result.OutputPath = $CliArgs[$index]
                }
            }
            '^--capture-kind(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.CaptureKind = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--capture-kind requires a value.' }
                    $result.CaptureKind = $CliArgs[$index]
                }
            }
            '^--historical-notice(?:=(.+))?$' {
                if ($Matches[1]) {
                    $result.HistoricalNotice = $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--historical-notice requires a value.' }
                    $result.HistoricalNotice = $CliArgs[$index]
                }
            }
            '^--compact$' { $result.Compact = $true }
            '^--ascii$' { $result.Ascii = $true }
            '^--no-color$' { $result.NoColor = $true }
            '^--recentcount(?:=(.+))?$' {
                $value = if ($Matches[1]) {
                    $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--RecentCount requires a value.' }
                    $CliArgs[$index]
                }

                if (-not [int]::TryParse([string]$value, [ref]$parsedValue) -or $parsedValue -le 0) {
                    throw '--RecentCount requires a positive integer.'
                }

                $result.RecentCount = $parsedValue
            }
            '^--barwidth(?:=(.+))?$' {
                $value = if ($Matches[1]) {
                    $Matches[1]
                }
                else {
                    $index++
                    if ($index -ge $CliArgs.Count) { throw '--BarWidth requires a value.' }
                    $CliArgs[$index]
                }

                if (-not [int]::TryParse([string]$value, [ref]$parsedValue) -or $parsedValue -le 0) {
                    throw '--BarWidth requires a positive integer.'
                }

                $result.BarWidth = $parsedValue
            }
            '^--json$' { $result.Json = $true }
            '^--team$' { $result.Team = $true }
            '^--worktrees$' { $result.Worktrees = $true }
            '^--preserve-existing-artifact$' { $result.PreserveExistingArtifact = $true }
            '^(?:-h|--help)$' { $result.Help = $true }
            default { throw ("Unknown argument for specrew where: {0}" -f $argument) }
        }
    }

    return [pscustomobject]$result
}

function ConvertTo-SpecrewWorktreeLines {
    param([object[]]$Worktrees)

    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('Specrew worktrees') | Out-Null
    $lines.Add('-----------------') | Out-Null

    foreach ($worktree in @($Worktrees)) {
        $featureLabel = if (-not [string]::IsNullOrWhiteSpace([string]$worktree.feature_number)) {
            [string]$worktree.feature_number
        }
        elseif (-not [string]::IsNullOrWhiteSpace([string]$worktree.feature_ref)) {
            [string]$worktree.feature_ref
        }
        else {
            '(none)'
        }

        $boundaryLabel = if (-not [string]::IsNullOrWhiteSpace([string]$worktree.boundary_type)) { [string]$worktree.boundary_type } else { '(none)' }
        $activityLabel = if (-not [string]::IsNullOrWhiteSpace([string]$worktree.last_activity)) { [string]$worktree.last_activity } else { '(none)' }
        $pathLine = [string]$worktree.path
        if (-not $worktree.exists -and -not [string]::IsNullOrWhiteSpace([string]$worktree.note)) {
            $pathLine = '{0} {1}' -f $pathLine, [string]$worktree.note
        }
        elseif (-not [string]::IsNullOrWhiteSpace([string]$worktree.note)) {
            $pathLine = '{0} {1}' -f $pathLine, [string]$worktree.note
        }

        $marker = if ($worktree.is_current) { '*' } else { '-' }
        $lines.Add(('{0} {1}' -f $marker, $pathLine)) | Out-Null
        $lines.Add((' Feature: {0}' -f $featureLabel)) | Out-Null
        $lines.Add((' Boundary: {0}' -f $boundaryLabel)) | Out-Null
        $lines.Add((' Last activity: {0}' -f $activityLabel)) | Out-Null
    }

    return $lines.ToArray()
}

function ConvertTo-SpecrewWorktreePayload {
    param([object[]]$Worktrees)

    return [pscustomobject]@{
        worktrees = @($Worktrees)
        lines     = @(ConvertTo-SpecrewWorktreeLines -Worktrees $Worktrees)
    }
}

try {
    $parsed = Convert-UnixStyleArguments `
        -ProjectPath $ProjectPath `
        -FeatureId $FeatureId `
        -IterationNumber $IterationNumber `
        -Compact $Compact.IsPresent `
        -Ascii $Ascii.IsPresent `
        -NoColor $NoColor.IsPresent `
        -RecentCount $RecentCount `
        -BarWidth $BarWidth `
        -Json $Json.IsPresent `
        -Team $Team.IsPresent `
        -Worktrees $Worktrees.IsPresent `
        -Help $Help.IsPresent `
        -OutputPath $OutputPath `
        -CaptureKind $CaptureKind `
        -PreserveExistingArtifact $PreserveExistingArtifact.IsPresent `
        -HistoricalNotice $HistoricalNotice `
        -CliArgs $CliArgs

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

    if ($parsed.Worktrees) {
        $worktreeState = @(Get-WorktreeState -ProjectRoot $parsed.ProjectPath)
        $lines = ConvertTo-SpecrewWorktreeLines -Worktrees $worktreeState

        if ($parsed.Json) {
            ConvertTo-SpecrewWorktreePayload -Worktrees $worktreeState | ConvertTo-Json -Depth 6
            exit 0
        }

        Write-SpecrewDashboardLines -Lines $lines -ColorMode $(if ($parsed.NoColor -or $parsed.Ascii) { 'plain' } else { 'plain' })
        exit 0
    }

    $snapshot = Get-SpecrewDashboardSnapshot `
        -ProjectRoot $parsed.ProjectPath `
        -FeatureId $parsed.FeatureId `
        -IterationNumber $parsed.IterationNumber `
        -Compact:$parsed.Compact `
        -Ascii:$parsed.Ascii `
        -NoColor:$parsed.NoColor `
        -RecentCount $parsed.RecentCount `
        -BarWidth $parsed.BarWidth `
        -CaptureKind $parsed.CaptureKind `
        -Team:$parsed.Team

    $lines = if ($parsed.Compact) {
        ConvertTo-SpecrewCompactDashboardLines -Snapshot $snapshot
    }
    else {
        ConvertTo-SpecrewDashboardLines -Snapshot $snapshot
    }

    if (-not [string]::IsNullOrWhiteSpace($parsed.OutputPath)) {
        $resolvedOutputPath = Resolve-ProjectPath -Path $parsed.OutputPath
        if ($parsed.PreserveExistingArtifact -and (Test-Path -LiteralPath $resolvedOutputPath -PathType Leaf)) {
            Write-Host "Preserved existing dashboard artifact at $resolvedOutputPath"
        }
        else {
            $artifactContent = ConvertTo-SpecrewDashboardArtifactContent -Snapshot $snapshot -Lines $lines -CaptureKind $parsed.CaptureKind -HistoricalNotice $parsed.HistoricalNotice
            Write-Utf8FileAtomic -Path $resolvedOutputPath -Content $artifactContent
            Write-Host "Wrote dashboard artifact to $resolvedOutputPath"
        }
    }

    if ($parsed.Json) {
        $payload = [pscustomobject]@{
            snapshot = $snapshot
            lines    = $lines
        }
        $payload | ConvertTo-Json -Depth 8
        exit 0
    }

    Write-SpecrewDashboardLines -Lines $lines -ColorMode $snapshot.color_mode
    exit 0
}
finally {
    Exit-SpecrewUtf8ConsoleScope -State $utf8ConsoleScope
}