Private/Set-PwshAsWindowsTerminalDefault.ps1
|
function Set-PwshAsWindowsTerminalDefault { # Points Windows Terminal's defaultProfile at the PowerShell 7 (PowershellCore) # profile. Only the defaultProfile value is rewritten; nothing else is touched. # # Write-Output, not Write-Host. This runs inside an Invoke-Step action, where # *>&1 merges the information stream into the pipeline: Write-Host would both # write to the console and emit an InformationRecord for Out-Default to # render, putting every line in the transcript twice. Write-Host remains # correct in Update-Everything's own summary, which is not inside a step. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Called from one step, which reports what it did and backs the file up before writing. The step, not this helper, is where a caller decides whether to proceed.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'The parameter belongs to a MatchEvaluator delegate signature and must be declared whether or not the body reads it.')] param([Parameter(Mandatory)][string] $LogDir) $candidates = @( "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json", "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json", "$env:LOCALAPPDATA\Microsoft\Windows Terminal\settings.json" ) # Indexed, not Select-Object -First: see the note in the Microsoft 365 step. $present = @($candidates | Where-Object { Test-Path -LiteralPath $_ }) $settingsPath = if ($present.Count) { $present[0] } else { $null } if (-not $settingsPath) { Write-Output 'Windows Terminal settings.json not found; Terminal not installed for this user. Skipping.' return } Write-Output "Windows Terminal settings: $settingsPath" # Terminal rewrites settings.json from memory when it exits, which silently # reverts an edit made while it is running. if (Get-Process -Name 'WindowsTerminal', 'WindowsTerminalPreview' -ErrorAction SilentlyContinue) { Write-Warning 'Windows Terminal is running. It rewrites settings.json on exit and may revert this change; close it and re-run if the default does not stick.' } $raw = Get-Content -Raw -LiteralPath $settingsPath -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { throw "settings.json at $settingsPath is empty; refusing to edit it." } $cfg = $null try { $cfg = Remove-JsonComment -Text $raw | ConvertFrom-Json -ErrorAction Stop } catch { Write-Warning "settings.json did not parse even after stripping comments: $($_.Exception.Message)" } # Stable V5 UUID the PowershellCore generator assigns to the primary pwsh # install; used as a fallback if the profile is not yet materialized in the file. $wellKnownPwsh = '{574e775e-4f2a-5b96-ac1e-a2962a402336}' $ps7Guid = $null if ($cfg -and $cfg.profiles -and $cfg.profiles.list) { $p = $cfg.profiles.list | Where-Object { $_.source -eq 'Windows.Terminal.PowershellCore' } | Select-Object -First 1 if (-not $p) { $p = $cfg.profiles.list | Where-Object { $_.commandline -match 'pwsh' } | Select-Object -First 1 } if ($p -and $p.guid) { $ps7Guid = $p.guid } } if (-not $ps7Guid) { $ps7Guid = $wellKnownPwsh Write-Output "No PowerShell Core profile present yet; using well-known GUID $ps7Guid (Terminal generates it on next launch)." } else { Write-Output "PowerShell 7 profile GUID: $ps7Guid" } # Read the current value from the parsed object when possible, and from the # raw text otherwise, so a parse failure cannot turn a no-op into a rewrite. $current = '' if ($cfg -and ($cfg.PSObject.Properties.Name -contains 'defaultProfile')) { $current = [string]$cfg.defaultProfile } elseif ($raw -match '"defaultProfile"\s*:\s*"([^"]*)"') { $current = $Matches[1] } if ($current -eq $ps7Guid) { Write-Output "Default profile is already PowerShell 7 ($ps7Guid). No change needed." return } Write-Output "Current default profile: $current" $backup = Join-Path $LogDir ("WindowsTerminal-settings-{0:yyyyMMdd-HHmmss}.json.bak" -f (Get-Date)) try { Copy-Item -LiteralPath $settingsPath -Destination $backup -Force -ErrorAction Stop } catch { throw "Could not back up settings.json to $backup; refusing to edit without a backup. $($_.Exception.Message)" } Write-Output "Backed up settings.json -> $backup" $newKv = '"defaultProfile": "' + $ps7Guid + '"' # Match any string value, not just a GUID: Terminal also accepts a profile # name here. A GUID-only pattern would miss that case and fall through to the # insert branch, producing a second defaultProfile key. $pattern = '"defaultProfile"\s*:\s*"[^"]*"' if ($raw -match $pattern) { $updated = [regex]::Replace($raw, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $newKv }) } else { # No defaultProfile key present: insert it right after the opening brace $updated = [regex]::Replace($raw, '\A(\s*\{)', [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $m.Groups[1].Value + "`r`n $newKv," }) } # Guard against the duplicate-key failure mode: Terminal takes the last one, # so two keys would make the edit look applied while doing nothing. $keyCount = ([regex]::Matches($updated, '"defaultProfile"\s*:')).Count if ($keyCount -ne 1) { throw "Refusing to write: the edit produced $keyCount 'defaultProfile' keys, expected exactly 1." } # Only trust the edit if the result is still valid JSON (when the original was) if ($cfg) { try { $null = Remove-JsonComment -Text $updated | ConvertFrom-Json -ErrorAction Stop } catch { throw "Refusing to write: edited settings.json is not valid JSON ($($_.Exception.Message))." } } try { [System.IO.File]::WriteAllText($settingsPath, $updated, [System.Text.UTF8Encoding]::new($false)) } catch { throw "Could not write settings.json (is Windows Terminal holding the file?): $($_.Exception.Message)" } Write-Output "Set Windows Terminal default profile to PowerShell 7 ($ps7Guid). Restart Windows Terminal to apply." } |