scripts/init/template-deploy.ps1
|
# Template deployment helpers for specrew-init.ps1 (extracted via Proposal 108 Slice 3) # # Depends on: scripts/init/_utilities.ps1 (Ensure-DirectoryExists, Get-SpecrewExecutionLayout, Add-Action) # # Functions: # - Copy-TemplateTree Recursive template copy with diff/preserve semantics # - Invoke-BundledTemplateDeployment Deploy .specify/.squad/.github trees from templates/ # - Test-BootstrappedProjectState Validate the .specify/.squad/.github expected layout exists Set-StrictMode -Version Latest function Copy-TemplateTree { param( [Parameter(Mandatory = $true)] [string]$SourceRoot, [Parameter(Mandatory = $true)] [string]$TargetRoot, [Parameter(Mandatory = $true)] [bool]$OverwriteExisting, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) if (-not (Test-Path -LiteralPath $SourceRoot -PathType Container)) { throw "Missing bundled template source '$SourceRoot'." } Ensure-DirectoryExists -Path $TargetRoot -PreviewOnly:$PreviewOnly $copied = 0 $updated = 0 $preserved = 0 $sourceFiles = @(Get-ChildItem -LiteralPath $SourceRoot -File -Recurse | Sort-Object FullName) foreach ($sourceFile in $sourceFiles) { $relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $sourceFile.FullName) $targetPath = Join-Path -Path $TargetRoot -ChildPath $relativePath $parent = Split-Path -Parent $targetPath if (-not [string]::IsNullOrWhiteSpace($parent)) { Ensure-DirectoryExists -Path $parent -PreviewOnly:$PreviewOnly } if (Test-Path -LiteralPath $targetPath -PathType Leaf) { if (-not $OverwriteExisting) { $preserved++ continue } $sourceContent = Get-Content -LiteralPath $sourceFile.FullName -Raw $targetContent = Get-Content -LiteralPath $targetPath -Raw if ($sourceContent -eq $targetContent) { $preserved++ continue } if (-not $PreviewOnly) { Copy-Item -LiteralPath $sourceFile.FullName -Destination $targetPath -Force } $updated++ continue } if (-not $PreviewOnly) { Copy-Item -LiteralPath $sourceFile.FullName -Destination $targetPath -Force } $copied++ } return [pscustomobject]@{ Copied = $copied Updated = $updated Preserved = $preserved Total = $sourceFiles.Count } } function Invoke-BundledTemplateDeployment { param( [Parameter(Mandatory = $true)] [pscustomobject]$ExecutionLayout, [Parameter(Mandatory = $true)] [string]$ProjectPath, [Parameter(Mandatory = $true)] [bool]$ForceRefresh, [Parameter(Mandatory = $true)] [bool]$SpecKitReady, [Parameter(Mandatory = $true)] [bool]$SquadReady, [Parameter(Mandatory = $true)] [bool]$HadSpecify, [Parameter(Mandatory = $true)] [bool]$HadSquad, [Parameter(Mandatory = $true)] [bool]$HadGitHub, [Parameter(Mandatory = $true)] [bool]$SpecKitExtensionOnly, [AllowEmptyCollection()] [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$Actions, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) if ([string]::IsNullOrWhiteSpace($ExecutionLayout.TemplateRoot)) { throw 'Bundled templates are unavailable for bootstrap.' } Add-Action -Actions $Actions -Step 'template-source' -Outcome ("{0}: {1}" -f $ExecutionLayout.Mode, $ExecutionLayout.TemplateRoot) $deployments = [System.Collections.ArrayList]::new() if ($SpecKitReady) { $null = $deployments.Add([pscustomobject]@{ Name = '.specify' SourceRoot = Join-Path -Path $ExecutionLayout.TemplateRoot -ChildPath 'specify' TargetRoot = Join-Path -Path $ProjectPath -ChildPath '.specify' HadExisting = $HadSpecify }) } if (-not $SpecKitExtensionOnly -and $SquadReady) { $null = $deployments.Add([pscustomobject]@{ Name = '.squad' SourceRoot = Join-Path -Path $ExecutionLayout.TemplateRoot -ChildPath 'squad' TargetRoot = Join-Path -Path $ProjectPath -ChildPath '.squad' HadExisting = $HadSquad }) } if (-not $SpecKitExtensionOnly) { $null = $deployments.Add([pscustomobject]@{ Name = '.github' SourceRoot = Join-Path -Path $ExecutionLayout.TemplateRoot -ChildPath 'github' TargetRoot = Join-Path -Path $ProjectPath -ChildPath '.github' HadExisting = $HadGitHub }) } foreach ($deployment in $deployments) { if ($deployment.HadExisting -and -not $ForceRefresh) { Add-Action -Actions $Actions -Step 'template-copy' -Outcome ("preserved existing {0}; re-run with -Force to refresh bundled templates" -f $deployment.Name) continue } $result = Copy-TemplateTree -SourceRoot $deployment.SourceRoot -TargetRoot $deployment.TargetRoot -OverwriteExisting:$ForceRefresh -PreviewOnly:$PreviewOnly $verb = if ($PreviewOnly) { 'would-sync' } else { 'synced' } Add-Action -Actions $Actions -Step 'template-copy' -Outcome ("{0} {1} from {2} ({3} new, {4} updated, {5} preserved)" -f $verb, $deployment.Name, $deployment.SourceRoot, $result.Copied, $result.Updated, $result.Preserved) } } function Test-BootstrappedProjectState { param( [Parameter(Mandatory = $true)] [string]$ProjectPath, [Parameter(Mandatory = $true)] [bool]$SpecKitExtensionOnly ) $failures = [System.Collections.Generic.List[string]]::new() $expectedSpecifyFiles = @( 'agent-file-template.md', 'checklist-template.md', 'constitution-template.md', 'plan-template.md', 'spec-template.md', 'tasks-template.md' ) $specifyTemplatesRoot = Join-Path -Path $ProjectPath -ChildPath '.specify' $specifyTemplatesRoot = Join-Path -Path $specifyTemplatesRoot -ChildPath 'templates' if (-not (Test-Path -LiteralPath $specifyTemplatesRoot -PathType Container)) { $failures.Add("Missing required template directory '$specifyTemplatesRoot'.") } else { foreach ($expectedFile in $expectedSpecifyFiles) { $expectedPath = Join-Path -Path $specifyTemplatesRoot -ChildPath $expectedFile if (-not (Test-Path -LiteralPath $expectedPath -PathType Leaf)) { $failures.Add("Missing required Spec Kit template '$expectedPath'.") } } } if (-not $SpecKitExtensionOnly) { $squadAgentsRoot = Join-Path -Path $ProjectPath -ChildPath '.squad' $squadAgentsRoot = Join-Path -Path $squadAgentsRoot -ChildPath 'agents' if (-not (Test-Path -LiteralPath $squadAgentsRoot -PathType Container)) { $failures.Add("Missing required Squad agents directory '$squadAgentsRoot'.") } $workflowRoot = Join-Path -Path $ProjectPath -ChildPath '.github' $coordinatorPromptPath = Join-Path -Path $workflowRoot -ChildPath 'agents\squad.agent.md' $workflowRoot = Join-Path -Path $workflowRoot -ChildPath 'workflows' if (-not (Test-Path -LiteralPath $workflowRoot -PathType Container)) { $failures.Add("Missing required workflow directory '$workflowRoot'.") } else { $workflowCount = @(Get-ChildItem -LiteralPath $workflowRoot -File -ErrorAction SilentlyContinue).Count if ($workflowCount -lt 1) { $failures.Add("Expected at least one workflow under '$workflowRoot'.") } } if (-not (Test-Path -LiteralPath $coordinatorPromptPath -PathType Leaf)) { $failures.Add("Missing required coordinator prompt '$coordinatorPromptPath'.") } } return [pscustomobject]@{ Succeeded = ($failures.Count -eq 0) Failures = @($failures) } } |