scripts/init/_utilities.ps1
|
# Shared utilities for specrew-init.ps1 (extracted via Proposal 108 Slice 1) # # Pure leaf — no dependencies on other init/ files. Behavior identical to the # original inline definitions in scripts/specrew-init.ps1 (extracted unchanged). # # Functions exported (all internal-only; called by specrew-init.ps1 and other init/*.ps1): # - Get-NativeExitCode read $LASTEXITCODE safely # - ConvertTo-YamlBoolean render bool as YAML "true"/"false" # - Test-ConsoleInputRedirected detect stdin redirection # - Write-Step cyan "==> ..." step header # - Invoke-NativeCommand run native exe; throw on non-zero # - Invoke-NativeCommandForOutput run native exe; capture stdout + exit code # - Invoke-WithNativeCommandEncoding force UTF-8 for specify on Windows # - Add-Action append @{Step,Outcome} to action list # - Ensure-DirectoryExists mkdir -p with PreviewOnly support # - Get-SpecrewExecutionLayout resolve module-vs-clone mode + TemplateRoot # - Write-MissingUtf8File idempotent UTF-8-no-BOM file write Set-StrictMode -Version Latest function Get-NativeExitCode { if (Get-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue) { return $global:LASTEXITCODE } return 0 } function ConvertTo-YamlBoolean { param( [Parameter(Mandatory = $true)] [bool]$Value ) if ($Value) { return 'true' } return 'false' } function Test-ConsoleInputRedirected { try { return [Console]::IsInputRedirected } catch { return $true } } function Write-Step { param( [Parameter(Mandatory = $true)] [string]$Message ) Write-Host ("==> {0}" -f $Message) -ForegroundColor Cyan } function Invoke-NativeCommand { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string[]]$ArgumentList, [Parameter(Mandatory = $true)] [string]$WorkingDirectory ) Push-Location $WorkingDirectory try { Invoke-WithNativeCommandEncoding -FilePath $FilePath -ScriptBlock { & $FilePath @ArgumentList if ((Get-NativeExitCode) -ne 0) { throw ("Command failed: {0} {1}" -f $FilePath, ($ArgumentList -join ' ')) } } } finally { Pop-Location } } function Invoke-NativeCommandForOutput { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string[]]$ArgumentList, [Parameter(Mandatory = $true)] [string]$WorkingDirectory ) Push-Location $WorkingDirectory try { $output = Invoke-WithNativeCommandEncoding -FilePath $FilePath -ScriptBlock { @(& $FilePath @ArgumentList 2>&1) } return [pscustomobject]@{ ExitCode = Get-NativeExitCode Output = @($output | ForEach-Object { [string]$_ }) } } finally { Pop-Location } } function Invoke-WithNativeCommandEncoding { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock ) $shouldForceUtf8 = $IsWindows -and $FilePath -eq 'specify' if (-not $shouldForceUtf8) { return & $ScriptBlock } $utf8 = [System.Text.UTF8Encoding]::new($false) $previousOutputEncoding = $null $previousInputEncoding = $null $previousPipelineEncoding = $OutputEncoding $previousPythonUtf8 = [Environment]::GetEnvironmentVariable('PYTHONUTF8', 'Process') $previousPythonIoEncoding = [Environment]::GetEnvironmentVariable('PYTHONIOENCODING', 'Process') try { $previousOutputEncoding = [Console]::OutputEncoding $previousInputEncoding = [Console]::InputEncoding } catch { $shouldForceUtf8 = $false } if (-not $shouldForceUtf8) { return & $ScriptBlock } try { [Console]::OutputEncoding = $utf8 [Console]::InputEncoding = $utf8 $script:OutputEncoding = $utf8 [Environment]::SetEnvironmentVariable('PYTHONUTF8', '1', 'Process') [Environment]::SetEnvironmentVariable('PYTHONIOENCODING', 'utf-8', 'Process') return & $ScriptBlock } finally { [Console]::OutputEncoding = $previousOutputEncoding [Console]::InputEncoding = $previousInputEncoding $script:OutputEncoding = $previousPipelineEncoding [Environment]::SetEnvironmentVariable('PYTHONUTF8', $previousPythonUtf8, 'Process') [Environment]::SetEnvironmentVariable('PYTHONIOENCODING', $previousPythonIoEncoding, 'Process') } } function Add-Action { param( [AllowEmptyCollection()] [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$Actions, [Parameter(Mandatory = $true)] [string]$Step, [Parameter(Mandatory = $true)] [string]$Outcome ) $null = $Actions.Add([pscustomobject]@{ Step = $Step Outcome = $Outcome }) } function Ensure-DirectoryExists { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) if (Test-Path -LiteralPath $Path) { return } if ($PreviewOnly) { return } New-Item -ItemType Directory -Path $Path -Force | Out-Null } function Get-SpecrewExecutionLayout { # Walk up from this file's location until we find the Specrew distribution root # (marked by Specrew.psd1). This is robust against file relocation — when this # function lived in scripts/specrew-init.ps1 the root was 1 level up; after the # Proposal 108 Slice 1 extraction to scripts/init/_utilities.ps1 the root is # 2 levels up. The marker-file walk works in both cases + any future relocation. $distributionRoot = $PSScriptRoot for ($i = 0; $i -lt 5; $i++) { if (Test-Path -LiteralPath (Join-Path $distributionRoot 'Specrew.psd1') -PathType Leaf) { break } $parent = Split-Path -Parent $distributionRoot if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $distributionRoot) { # Reached filesystem root without finding the marker — fall back to legacy 1-up resolution $distributionRoot = Split-Path -Parent $PSScriptRoot break } $distributionRoot = $parent } $templateRoot = Join-Path -Path $distributionRoot -ChildPath 'templates' $isModuleLayout = $env:SPECREW_INVOKED_FROM_MODULE -eq '1' return [pscustomobject]@{ RootPath = $distributionRoot Mode = $(if ($isModuleLayout) { 'module' } else { 'clone' }) TemplateRoot = $(if (Test-Path -LiteralPath $templateRoot -PathType Container) { $templateRoot } else { $null }) } } function Write-MissingUtf8File { param( [Parameter(Mandatory = $true)] [string]$Path, [AllowEmptyString()] [Parameter(Mandatory = $true)] [string]$Content, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) if (Test-Path -LiteralPath $Path) { return } if ($PreviewOnly) { return } $parent = Split-Path -Parent $Path if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false)) } |