lib/ConfigScaffold.ps1
|
function New-ConfigFromTemplate { <# .SYNOPSIS Copies the bundled Initialize-DeveloperMachine.config.json template (passed via -TemplatePath, typically the file shipped next to the entry-point script) to -Path. Creates the parent directory if it doesn't exist. Throws when the destination already exists - pass -Force to overwrite. We read from the bundled file rather than embedding a string literal here so there's a single source of truth for the starter config. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $TemplatePath, [Parameter()] [switch] $Force ) if (-not (Test-Path -LiteralPath $TemplatePath -PathType Leaf)) { throw "Config template not found at '$TemplatePath'." } if (-not $Force -and (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Config already exists at '$Path'. Pass -Force to overwrite." } $dir = Split-Path -Parent $Path if ($dir -and -not (Test-Path -LiteralPath $dir -PathType Container)) { if ($PSCmdlet.ShouldProcess($dir, 'Create parent directory')) { $null = New-Item -ItemType Directory -Path $dir -Force } } if ($PSCmdlet.ShouldProcess($Path, "Copy template from '$TemplatePath'")) { $content = Get-Content -LiteralPath $TemplatePath -Raw -Encoding UTF8 # UTF-8 without BOM, atomic write. [System.IO.File]::WriteAllText( $Path, $content, [System.Text.UTF8Encoding]::new($false) ) } } function Show-ConfigPreview { <# .SYNOPSIS Prints the contents of a config file to the host with a gray background and white text so it stands out from regular log output. Falls back to plain text on hosts without VT support. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Write-Warning "Config file not found at: $Path" return } $content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 $lines = $content -split "`r?`n" if ($script:UseColor) { $esc = [char]27 $bg = "$esc[48;5;238m" # medium-gray background $fg = "$esc[97m" # bright white text $reset = "$esc[0m" # Pad each line to a consistent visual width so the gray # block reads as a single rectangle even with ragged content. $width = ($lines | Measure-Object -Property Length -Maximum).Maximum Write-Host '' foreach ($line in $lines) { $padded = $line.PadRight($width) Write-Host "$bg$fg $padded $reset" } Write-Host '' } else { Write-Host '' Write-Host '----- config -----' foreach ($line in $lines) { Write-Host $line } Write-Host '------------------' Write-Host '' } } function Read-YesNo { <# .SYNOPSIS Prompts the user for a Y/N answer. Default is Y when the user just hits Enter. Returns $true for yes, $false for no. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Prompt ) while ($true) { $answer = Read-Host -Prompt "$Prompt [Y/n]" if (-not $answer) { return $true } switch -Regex ($answer.Trim().ToLowerInvariant()) { '^(y|yes)$' { return $true } '^(n|no)$' { return $false } default { Write-Host "Please answer Y or N." } } } } function Read-ProceedOrEdit { <# .SYNOPSIS Prompts the user with two choices: P - proceed with the current config E - open the file in the default editor and wait for save, then reload from disk Returns the upper-case letter chosen. Re-asks on invalid input. (Quit was removed - users can Ctrl+C if they really need to bail.) #> [CmdletBinding()] param() while ($true) { $answer = Read-Host -Prompt '[P]roceed or [E]dit and reload' switch -Regex ($answer.Trim().ToUpperInvariant()) { '^P$' { return 'P' } '^E$' { return 'E' } default { Write-Host 'Please pick P or E.' } } } } function Open-EditorAndWaitForSave { <# .SYNOPSIS Opens $Path in the OS default editor (Start-Process uses the file association for .json - Notepad, VS Code, whatever the user has configured), then calls Wait-ForFileSave to spin until the file is saved. If Start-Process can't launch (no association, denied, etc.) the function logs a [WARN] and falls through to Wait-ForFileSave anyway - the user can open the file manually and the spinner will still detect the save. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter()] [int] $TimeoutSeconds = 1800, [Parameter()] [int] $PollIntervalMs = 500 ) try { Start-Process -FilePath $Path -ErrorAction Stop | Out-Null } catch { Write-Status -Level Warn -Message " [WARN] Couldn't auto-open '$Path' ($($_.Exception.Message)). Open it manually." } Wait-ForFileSave -Path $Path -TimeoutSeconds $TimeoutSeconds -PollIntervalMs $PollIntervalMs } function Wait-ForFileSave { <# .SYNOPSIS Polls $Path's LastWriteTimeUtc until it changes, indicating the user saved the file in whatever editor they prefer. Shows the script's spinner with a "Waiting for changes - save the file to continue" message so the terminal isn't silent. Returns $true when a save was detected, $false on timeout. Default timeout is 30 min (generous - users can take a while to compose a config). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter()] [int] $TimeoutSeconds = 1800, [Parameter()] [int] $PollIntervalMs = 500 ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "File not found: $Path" } $initial = (Get-Item -LiteralPath $Path -Force).LastWriteTimeUtc $spinner = New-Spinner -InitialMessage 'Waiting for changes - save the file to continue...' try { $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds $PollIntervalMs $current = (Get-Item -LiteralPath $Path -Force).LastWriteTimeUtc if ($current -ne $initial) { return $true } } return $false } finally { Stop-Spinner -Spinner $spinner } } |