#!/usr/bin/env pwsh # Common PowerShell functions analogous to common.sh function Get-RepoRoot { try { $result = git rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -eq 0) { return $result } } catch { # Git command failed } # Fall back to script location for non-git repos return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path } function Get-CurrentBranch { # First check if SPECIFY_FEATURE environment variable is set if ($env:SPECIFY_FEATURE) { return $env:SPECIFY_FEATURE } # Then check git if available try { $result = git rev-parse --abbrev-ref HEAD 2>$null if ($LASTEXITCODE -eq 0) { return $result } } catch { # Git command failed } # For non-git repos, try to find the latest feature directory $repoRoot = Get-RepoRoot $specsDir = Join-Path $repoRoot "specs" if (Test-Path $specsDir) { $latestFeature = "" $highest = 0 Get-ChildItem -Path $specsDir -Directory | ForEach-Object { if ($_.Name -match '^(\d{3})-') { $num = [int]$matches[1] if ($num -gt $highest) { $highest = $num $latestFeature = $_.Name } } } if ($latestFeature) { return $latestFeature } } # Final fallback return "main" } function Test-HasGit { try { git rev-parse --show-toplevel 2>$null | Out-Null return ($LASTEXITCODE -eq 0) } catch { return $false } } function Test-FeatureBranch { param( [string]$Branch, [bool]$HasGit = $true ) # For non-git repos, we can't enforce branch naming but still provide output if (-not $HasGit) { Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation" return $true } if ($Branch -notmatch '^[0-9]{3}-') { Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" Write-Output "Feature branches should be named like: 001-feature-name" return $false } return $true } function Get-FeatureDir { param([string]$RepoRoot, [string]$Branch) Join-Path $RepoRoot "specs/$Branch" } function Get-FeaturePathsEnv { $repoRoot = Get-RepoRoot $currentBranch = Get-CurrentBranch $hasGit = Test-HasGit $featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch [PSCustomObject]@{ REPO_ROOT = $repoRoot CURRENT_BRANCH = $currentBranch HAS_GIT = $hasGit FEATURE_DIR = $featureDir FEATURE_SPEC = Join-Path $featureDir 'spec.md' IMPL_PLAN = Join-Path $featureDir 'plan.md' TASKS = Join-Path $featureDir 'tasks.md' RESEARCH = Join-Path $featureDir 'research.md' DATA_MODEL = Join-Path $featureDir 'data-model.md' QUICKSTART = Join-Path $featureDir 'quickstart.md' CONTRACTS_DIR = Join-Path $featureDir 'contracts' } } function Test-FileExists { param([string]$Path, [string]$Description) if (Test-Path -Path $Path -PathType Leaf) { Write-Output " ✓ $Description" return $true } else { Write-Output " ✗ $Description" return $false } } function Test-DirHasFiles { param([string]$Path, [string]$Description) if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) { Write-Output " ✓ $Description" return $true } else { Write-Output " ✗ $Description" return $false } } # Resolve a template name to a file path using the priority stack: # 1. .specify/templates/overrides/ # 2. .specify/presets//templates/ (sorted by priority from .registry) # 3. .specify/extensions//templates/ # 4. .specify/templates/ (core) function Resolve-Template { param( [Parameter(Mandatory=$true)][string]$TemplateName, [Parameter(Mandatory=$true)][string]$RepoRoot ) $base = Join-Path $RepoRoot '.specify/templates' # Priority 1: Project overrides $override = Join-Path $base "overrides/$TemplateName.md" if (Test-Path $override) { return $override } # Priority 2: Installed presets (sorted by priority from .registry) $presetsDir = Join-Path $RepoRoot '.specify/presets' if (Test-Path $presetsDir) { $registryFile = Join-Path $presetsDir '.registry' $sortedPresets = @() if (Test-Path $registryFile) { try { $registryData = Get-Content $registryFile -Raw | ConvertFrom-Json $presets = $registryData.presets if ($presets) { $sortedPresets = $presets.PSObject.Properties | Sort-Object { if ($_.Value.priority) { $_.Value.priority } else { 10 } } | ForEach-Object { $_.Name } } } catch { # Fallback: alphabetical directory order $sortedPresets = @() } } if ($sortedPresets.Count -gt 0) { foreach ($presetId in $sortedPresets) { $candidate = Join-Path $presetsDir "$presetId/templates/$TemplateName.md" if (Test-Path $candidate) { return $candidate } } } else { # Fallback: alphabetical directory order foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) { $candidate = Join-Path $preset.FullName "templates/$TemplateName.md" if (Test-Path $candidate) { return $candidate } } } } # Priority 3: Extension-provided templates $extDir = Join-Path $RepoRoot '.specify/extensions' if (Test-Path $extDir) { foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) { $candidate = Join-Path $ext.FullName "templates/$TemplateName.md" if (Test-Path $candidate) { return $candidate } } } # Priority 4: Core templates $core = Join-Path $base "$TemplateName.md" if (Test-Path $core) { return $core } return $null }