scripts/init/spec-kit-deploy.ps1
|
# Spec-Kit deployment helpers for specrew-init.ps1 (extracted via Proposal 108 Slice 4) # # Depends on: scripts/init/_utilities.ps1 (Invoke-NativeCommand*, Get-FirstNonEmptyOutputLine, # Add-Action, Invoke-WithNativeCommandEncoding); shared-governance.ps1 (Write-Utf8FileAtomic). # # Functions: # - Get-SpecKitGitReference normalize "0.8.4" → "v0.8.4" # - Get-SpecKitInstallArguments build "uv tool install" argv # - Get-SpecKitInstallCommandText render install command string for display # - Get-FirstNonEmptyOutputLine first non-blank line of probe output # - Test-SpecifyReleaseAssetBlocker detect upstream Spec Kit release-asset 404 # - Test-SpecifyExtensionAddAvailable probe "specify extension add --help" # - Test-SpecifyInitPreflight dry-probe "specify init" + auto-repair stale specify # - Get-SpecifyInitPreflightResult build preflight result object # - Invoke-SpecKitExtensionDeployment deploy specrew-speckit extension into project Set-StrictMode -Version Latest function Get-SpecKitGitReference { param( [Parameter(Mandatory = $true)] [string]$Version ) $trimmedVersion = $Version.Trim() if ([string]::IsNullOrWhiteSpace($trimmedVersion)) { throw 'Spec Kit version cannot be empty.' } if ($trimmedVersion.StartsWith('v', [System.StringComparison]::OrdinalIgnoreCase)) { return $trimmedVersion } return ('v{0}' -f $trimmedVersion) } function Get-SpecKitInstallArguments { param( [Parameter(Mandatory = $true)] [string]$Version, [Parameter(Mandatory = $true)] [bool]$ForceInstall ) $arguments = @('tool', 'install') if ($ForceInstall) { $arguments += '--force' } $arguments += @( 'specify-cli', '--from', ('git+https://github.com/github/spec-kit.git@{0}' -f (Get-SpecKitGitReference -Version $Version)) ) return $arguments } function Get-SpecKitInstallCommandText { param( [Parameter(Mandatory = $true)] [string]$Version, [Parameter(Mandatory = $true)] [bool]$ForceInstall ) return ('uv {0}' -f ((Get-SpecKitInstallArguments -Version $Version -ForceInstall $ForceInstall) -join ' ')) } function Get-FirstNonEmptyOutputLine { param( [AllowEmptyCollection()] [string[]]$OutputLines ) return @($OutputLines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1)[0] } function Test-SpecifyReleaseAssetBlocker { param( [AllowEmptyCollection()] [string[]]$OutputLines ) $combinedOutput = (@($OutputLines) -join [Environment]::NewLine) return $combinedOutput -match 'No matching release asset found for .+spec-kit-template-' } function Test-SpecifyExtensionAddAvailable { param( [Parameter(Mandatory = $true)] [string]$WorkingDirectory ) try { $probe = Invoke-NativeCommandForOutput -FilePath 'specify' -ArgumentList @('extension', 'add', '--help') -WorkingDirectory $WorkingDirectory } catch { return $false } if ($probe.ExitCode -ne 0) { return $false } $output = ($probe.Output -join [Environment]::NewLine) return $output -match 'specify extension add' -and $output -match 'Install an extension' } function Test-SpecifyInitPreflight { param( [Parameter(Mandatory = $true)] [string]$ProjectPath, [Parameter(Mandatory = $true)] [string[]]$ArgumentList, [Parameter(Mandatory = $true)] [string]$SpecKitVersion ) $probeDirectory = Join-Path $ProjectPath ('.specrew-specify-probe-{0}' -f [guid]::NewGuid().ToString('N')) New-Item -Path $probeDirectory -ItemType Directory -Force | Out-Null try { $probeResult = Invoke-NativeCommandForOutput -FilePath 'specify' -ArgumentList $ArgumentList -WorkingDirectory $probeDirectory if ($probeResult.ExitCode -eq 0) { return Get-SpecifyInitPreflightResult -Ready $true -Repaired $false -RepairOutcome $null -FailureMessage $null } $failureSummary = Get-FirstNonEmptyOutputLine -OutputLines $probeResult.Output if (-not (Test-SpecifyReleaseAssetBlocker -OutputLines $probeResult.Output)) { return Get-SpecifyInitPreflightResult -Ready $false -Repaired $false -RepairOutcome $null -FailureMessage ("Spec Kit preflight failed before Specrew touched your project: {0}" -f $(if ($failureSummary) { $failureSummary } else { 'specify init exited without any diagnostic output' })) } if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { return Get-SpecifyInitPreflightResult -Ready $false -Repaired $false -RepairOutcome $null -FailureMessage ("Spec Kit preflight hit the upstream release-asset blocker ({0}). Install the official GitHub release with '{1}', then re-run specrew init." -f $failureSummary, (Get-SpecKitInstallCommandText -Version $SpecKitVersion -ForceInstall $true)) } Write-Host ("[info] Detected Spec Kit release-asset blocker during preflight; reinstalling official Spec Kit {0} from GitHub." -f (Get-SpecKitGitReference -Version $SpecKitVersion)) -ForegroundColor Yellow $repairResult = Invoke-NativeCommandForOutput -FilePath 'uv' -ArgumentList (Get-SpecKitInstallArguments -Version $SpecKitVersion -ForceInstall $true) -WorkingDirectory $probeDirectory foreach ($line in @($repairResult.Output)) { if (-not [string]::IsNullOrWhiteSpace($line)) { Write-Host $line } } if ($repairResult.ExitCode -ne 0) { $repairFailureSummary = Get-FirstNonEmptyOutputLine -OutputLines $repairResult.Output return Get-SpecifyInitPreflightResult -Ready $false -Repaired $false -RepairOutcome $null -FailureMessage ("Spec Kit preflight hit the upstream release-asset blocker ({0}), and automatic repair failed{1}. Run '{2}' manually, then re-run specrew init." -f $failureSummary, $(if ($repairFailureSummary) { ": $repairFailureSummary" } else { '' }), (Get-SpecKitInstallCommandText -Version $SpecKitVersion -ForceInstall $true)) } $retryResult = Invoke-NativeCommandForOutput -FilePath 'specify' -ArgumentList $ArgumentList -WorkingDirectory $probeDirectory if ($retryResult.ExitCode -eq 0) { return Get-SpecifyInitPreflightResult -Ready $true -Repaired $true -RepairOutcome ("reinstalled Spec Kit from official GitHub release {0}" -f (Get-SpecKitGitReference -Version $SpecKitVersion)) -FailureMessage $null } $retryFailureSummary = Get-FirstNonEmptyOutputLine -OutputLines $retryResult.Output return Get-SpecifyInitPreflightResult -Ready $false -Repaired $true -RepairOutcome ("reinstalled Spec Kit from official GitHub release {0}" -f (Get-SpecKitGitReference -Version $SpecKitVersion)) -FailureMessage ("Spec Kit was repaired to the official GitHub release, but `specify init` still failed in preflight: {0}" -f $(if ($retryFailureSummary) { $retryFailureSummary } else { 'specify init exited without any diagnostic output' })) } finally { if (Test-Path -LiteralPath $probeDirectory) { Remove-Item -LiteralPath $probeDirectory -Recurse -Force -ErrorAction SilentlyContinue } } } function Get-SpecifyInitPreflightResult { param( [Parameter(Mandatory = $true)] [bool]$Ready, [Parameter(Mandatory = $true)] [bool]$Repaired, [AllowNull()] [string]$RepairOutcome, [AllowNull()] [string]$FailureMessage ) return [pscustomobject]@{ Ready = $Ready Repaired = $Repaired RepairOutcome = $RepairOutcome FailureMessage = $FailureMessage } } function Invoke-SpecKitExtensionDeployment { param( [Parameter(Mandatory = $true)] [string]$ProjectPath, [Parameter(Mandatory = $true)] [string]$RepoRoot, [Parameter(Mandatory = $true)] [string]$FallbackScriptPath, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) $targetExtensionRoot = Join-Path $ProjectPath '.specify\extensions\specrew-speckit' if (Test-Path -LiteralPath $targetExtensionRoot) { return [pscustomobject]@{ Action = 'preserved' Path = $targetExtensionRoot } } $extensionSourceRoot = Join-Path $RepoRoot 'extensions\specrew-speckit' if (Test-SpecifyExtensionAddAvailable -WorkingDirectory $ProjectPath) { if ($PreviewOnly) { Write-Host ("[dry-run] specify extension add --dev {0}" -f $extensionSourceRoot) -ForegroundColor Yellow return [pscustomobject]@{ Action = 'would-install-via-cli' Path = $targetExtensionRoot } } try { Invoke-NativeCommand -FilePath 'specify' -ArgumentList @('extension', 'add', '--dev', $extensionSourceRoot) -WorkingDirectory $ProjectPath return [pscustomobject]@{ Action = 'installed-via-cli' Path = $targetExtensionRoot } } catch { Write-Host '[info] specify extension add failed; falling back to manual Specrew extension deployment.' -ForegroundColor Yellow } } $null = @( & $FallbackScriptPath ` -ProjectPath $ProjectPath ` -DryRun:$PreviewOnly ` -PassThru ) return [pscustomobject]@{ Action = $(if ($PreviewOnly) { 'would-install-manual-fallback' } else { 'installed-manual-fallback' }) Path = $targetExtensionRoot } } |