Specrew.psm1

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

$ScriptRoot = $PSScriptRoot
$scriptsPath = Join-Path -Path $ScriptRoot -ChildPath 'scripts'
$internalScriptsPath = Join-Path -Path $scriptsPath -ChildPath 'internal'

. (Join-Path -Path $internalScriptsPath -ChildPath 'dashboard-renderer.ps1')

$script:SpecrewScriptMap = [ordered]@{
    'specrew'        = Join-Path -Path $scriptsPath -ChildPath 'specrew.ps1'
    'specrew-init'   = Join-Path -Path $scriptsPath -ChildPath 'specrew-init.ps1'
    'specrew-review' = Join-Path -Path $scriptsPath -ChildPath 'specrew-review.ps1'
    'specrew-start'  = Join-Path -Path $scriptsPath -ChildPath 'specrew-start.ps1'
    'specrew-team'   = Join-Path -Path $scriptsPath -ChildPath 'specrew-team.ps1'
    'specrew-update' = Join-Path -Path $scriptsPath -ChildPath 'specrew-update.ps1'
    'specrew-version' = Join-Path -Path $scriptsPath -ChildPath 'specrew-version.ps1'
    'specrew-where'  = Join-Path -Path $scriptsPath -ChildPath 'specrew-where.ps1'
}

function Invoke-SpecrewScript {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandName,

        [Parameter(ValueFromRemainingArguments = $true)]
        [object[]]$Arguments
    )

    $scriptPath = $script:SpecrewScriptMap[$CommandName]
    if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
        throw "Missing Specrew script '$scriptPath'."
    }

    $forwardedArguments = @($Arguments)
    if ($forwardedArguments.Count -eq 1 -and $forwardedArguments[0] -is [System.Array]) {
        $forwardedArguments = @($forwardedArguments[0])
    }

    # On Linux/macOS, `specrew start` needs a special launch path because
    # PowerShell on Linux strips TTY from native command children when invoked
    # from a script body (empirically verified: even nano's TUI fails to
    # render when launched via `& nano` inside a .ps1). PowerShell FUNCTION
    # bodies, however, do preserve TTY. So for `specrew start` on Linux/macOS:
    #
    # 1. The script (specrew-start.ps1) does all prep work but writes the
    # final `copilot` launch args to a deferred-launch file instead of
    # invoking copilot itself.
    # 2. After the script returns, THIS function (Invoke-SpecrewScript) reads
    # the deferred-launch file and invokes `& copilot @args` from its own
    # body — function context, TTY preserved → Copilot TUI renders.
    #
    # The user-facing command is typically `specrew start` (CommandName =
    # 'specrew', first argument = 'start'); the direct `specrew-start`
    # function form is also supported. Both forms route here.
    $isStartCommand = (
        ($CommandName -eq 'specrew-start') -or
        ($CommandName -eq 'specrew' -and
         $forwardedArguments.Count -gt 0 -and
         "$($forwardedArguments[0])" -eq 'start')
    )
    $needsDeferredLaunch = $isStartCommand -and -not $IsWindows

    $deferredLaunchFile = $null
    if ($needsDeferredLaunch) {
        $deferredLaunchFile = [System.IO.Path]::Combine(
            [System.IO.Path]::GetTempPath(),
            "specrew-deferred-launch-$([guid]::NewGuid().ToString()).json"
        )
        $env:SPECREW_DEFERRED_LAUNCH_FILE = $deferredLaunchFile
    }

    $env:SPECREW_INVOKED_FROM_MODULE = '1'
    try {
        if ($needsDeferredLaunch) {
            # In-process invocation so the script can write the deferred-launch
            # file to a location this function can read after the script returns.
            & $scriptPath @forwardedArguments
        }
        else {
            & pwsh -NoProfile -ExecutionPolicy Bypass -File $scriptPath @forwardedArguments
        }

        # After the script returns, check for a deferred launch request.
        if ($needsDeferredLaunch -and (Test-Path -LiteralPath $deferredLaunchFile -PathType Leaf)) {
            try {
                $launchInfo = Get-Content -LiteralPath $deferredLaunchFile -Raw -Encoding UTF8 | ConvertFrom-Json
                $copilotPath = [string]$launchInfo.CopilotPath
                $copilotArgs = @($launchInfo.CopilotArgs)
                $workingDirectory = [string]$launchInfo.WorkingDirectory

                Push-Location -LiteralPath $workingDirectory
                try {
                    # Function-body invocation: PowerShell on Linux preserves
                    # TTY for native command children when called from a
                    # function body (vs a script body which strips it).
                    & $copilotPath @copilotArgs
                }
                finally {
                    Pop-Location
                }
            }
            finally {
                Remove-Item -LiteralPath $deferredLaunchFile -Force -ErrorAction SilentlyContinue
            }
        }
    }
    finally {
        Remove-Item -LiteralPath 'env:SPECREW_INVOKED_FROM_MODULE' -ErrorAction SilentlyContinue
        if ($needsDeferredLaunch) {
            Remove-Item -LiteralPath 'env:SPECREW_DEFERRED_LAUNCH_FILE' -ErrorAction SilentlyContinue
        }
    }
}

# Functions use PowerShell's approved Verb-Noun naming convention so
# `Import-Module Specrew.psd1` does NOT emit the "unapproved verbs" warning.
# The CLI-friendly names users actually type (`specrew`, `specrew-start`,
# `specrew-init`, etc.) are exposed as aliases below — aliases don't trigger
# the verb-check warning, so users keep their muscle memory.

function Invoke-Specrew {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew' -Arguments $Arguments
}

function Initialize-Specrew {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-init' -Arguments $Arguments
}

function Show-SpecrewReview {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-review' -Arguments $Arguments
}

function Start-Specrew {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-start' -Arguments $Arguments
}

function Invoke-SpecrewTeam {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-team' -Arguments $Arguments
}

function Update-Specrew {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-update' -Arguments $Arguments
}

function Show-SpecrewVersion {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-version' -Arguments $Arguments
}

function Show-SpecrewStatus {
    [CmdletBinding()]
    param([Parameter(Position = 0, ValueFromRemainingArguments = $true)][object[]]$Arguments)
    Invoke-SpecrewScript -CommandName 'specrew-where' -Arguments $Arguments
}

# CLI-friendly aliases so users continue typing the names they already know.
# These don't trigger the unapproved-verb warning that the function names did.
Set-Alias -Name 'specrew'         -Value 'Invoke-Specrew'        -Force
Set-Alias -Name 'specrew-init'    -Value 'Initialize-Specrew'    -Force
Set-Alias -Name 'specrew-review'  -Value 'Show-SpecrewReview'    -Force
Set-Alias -Name 'specrew-start'   -Value 'Start-Specrew'         -Force
Set-Alias -Name 'specrew-team'    -Value 'Invoke-SpecrewTeam'    -Force
Set-Alias -Name 'specrew-update'  -Value 'Update-Specrew'        -Force
Set-Alias -Name 'specrew-version' -Value 'Show-SpecrewVersion'   -Force
Set-Alias -Name 'specrew-where'   -Value 'Show-SpecrewStatus'    -Force

Export-ModuleMember `
    -Function @(
        'Invoke-Specrew',
        'Initialize-Specrew',
        'Start-Specrew',
        'Update-Specrew',
        'Show-SpecrewVersion',
        'Show-SpecrewReview',
        'Invoke-SpecrewTeam',
        'Show-SpecrewStatus'
    ) `
    -Alias @(
        'specrew',
        'specrew-init',
        'specrew-start',
        'specrew-update',
        'specrew-version',
        'specrew-review',
        'specrew-team',
        'specrew-where'
    )

# SIG # Begin signature block
# MIIFxQYJKoZIhvcNAQcCoIIFtjCCBbICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCArmpgXg++JG5h
# /NT/MgObr/i6kN6SKEcRRhjYg059RqCCAyowggMmMIICDqADAgECAhAvrt7E+d2j
# hER/OkApTCvkMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFNwZWNyZXcgU2Vs
# Zi1TaWduZWQgQ29kZSBTaWduaW5nMB4XDTI2MDUxOTIwMTg1M1oXDTI3MDUxODIw
# Mjg1M1owKzEpMCcGA1UEAwwgU3BlY3JldyBTZWxmLVNpZ25lZCBDb2RlIFNpZ25p
# bmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyci+zahW58wAz5/F1
# ZOEJXY6F9fbIoHKnXrnM0Fh43mnmrD5T7UYBZhAMc4dBew3dswTveWl34ZK8IkBS
# hNTunpMx6CJBNJVeLo+F36lfsffXKgs/fPg9l0gumVlfOqmMj7SDGR5oprdj1yn4
# xI+pi5Mlw061Nahb0SF58BqE+nOpJovoOV/jxeTUjB/2/5+JGug6xb23AQtM8BBU
# 0CZMFHZ+DwXlJy1cfPOcqcE6IKLX5nA9+ybXua8TVQtrXJ5MKnFmA8jFVqLcDIvC
# tgu8r7TUYICacd0DjH1/x+TuLwYJuHQcsbVFtZ0ZwIFDBgNTa33NJYpnq9q1Z4UQ
# rqoVAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
# AzAdBgNVHQ4EFgQUxBXZ6s9QZP3+RxN+nxYotddn22YwDQYJKoZIhvcNAQELBQAD
# ggEBAF1aa2KnTpw4bvLUD5r5tdiv2vR3Yj62VGwNQcbyY4ws2GFKk0FtBnGSE3Bj
# tzPi8cx2oUfP0KQR4qIz+7f58gThr/+hdiDCNTcdnhtlFFrRxwXJguvKG4YDcKfb
# HNhuLCCeHJC/qXK6Chycwh0vmK/dFqiPCQHiNry1YAVxC2T4mHtr5xV2MQjN9n2b
# KsDKSFIrVg2Z6irqaJR6eFh5a8lnPdVliFy6AWEunaAC26PqcdAkNX2sbw5tvnln
# tWYa8guu6P3RWPFQ3ciuHDYNbrb2Puj1PjC2geymGboAUAWH9rRQIhPjQMdLnPj/
# Y77gMazFFIAffY9bxR5t+QS7xD8xggHxMIIB7QIBATA/MCsxKTAnBgNVBAMMIFNw
# ZWNyZXcgU2VsZi1TaWduZWQgQ29kZSBTaWduaW5nAhAvrt7E+d2jhER/OkApTCvk
# MA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJ
# KoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQB
# gjcCARUwLwYJKoZIhvcNAQkEMSIEINzyTX673CB6P9IOngyQUHiGteyOMsDBC8IJ
# annW15FeMA0GCSqGSIb3DQEBAQUABIIBAKDoa+FbzWf0s0k/PuPypODsfQ9pF0ae
# 4RP08LbXaZYqyFsN0aRLxdl1bqFXNmU51vtC9nw68HVrUtIujves+BYRSCfFObMp
# i6ddz6HTg0iWIJ89VXJXEypZn1Ez9a3lIBZS9wWn4QSPmW8ig/m/frcXSGOgurOk
# p2R+wwlg744CcRmTr2b4tcAyOIklt81b3jdnM7i8FzdoYar0pc2bhqT7yOBEGxgF
# vQH9+WPI3wxxJxiAAQyN9QHyxgnwORHRTtykoXVipYlUVPgXou2Mn+AttTds7IvI
# XyfQszwXYhe9IQsW4SlgwgFZK2ttix/MYJnPSytJLBpKNmR6uYK7w2M=
# SIG # End signature block