Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions Microsoft.PowerShell_profile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,18 @@ function Update-Profile {
foreach ($dir in $profileDirs) {
$target = Join-Path $dir "Microsoft.PowerShell_profile.ps1"
if (Test-Path $dir) {
# Back up the current profile before overwriting it, keeping the latest five
# (profile.original.ps1 is never matched, so the first-install copy survives).
if (Test-Path -Path $target -PathType Leaf) {
try {
$backupStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
Copy-Item -Path $target -Destination (Join-Path $dir "oldprofile.$backupStamp.ps1") -Force -ErrorAction Stop
Get-ChildItem -Path $dir -Filter 'oldprofile*.ps1' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -Skip 5 |
ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue }
}
catch { Write-Warning "Could not back up $target before update: $_" }
}
try {
Copy-Item -Path $tempProfile -Destination $target -Force -ErrorAction Stop
$copySuccess++
Expand Down Expand Up @@ -1991,10 +2003,14 @@ function sed($file, $find, $replace) {
}
if ($null -eq $find -or $find -eq '') { Write-Error "Usage: sed <file> <find> <replace>"; return }
if ($null -eq $replace) { $replace = '' }
$content = Get-Content -LiteralPath $file -Raw
$resolved = (Resolve-Path -LiteralPath $file).Path
# Read as UTF-8 (BOM-aware) so non-ASCII content survives the rewrite on Windows PowerShell 5.1.
$content = Get-Utf8FileText $resolved
if ($null -eq $content -or $content.Length -eq 0) { Write-Warning "File is empty: $file"; return }
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText((Resolve-Path -LiteralPath $file).Path, $content.replace("$find", $replace), $utf8NoBom)
# Keep a single .bak of the pre-edit content, like sed -i.bak.
Copy-Item -LiteralPath $resolved -Destination "$resolved.bak" -Force -ErrorAction SilentlyContinue
[System.IO.File]::WriteAllText($resolved, $content.replace("$find", $replace), $utf8NoBom)
}

# Show the full path of a command
Expand Down Expand Up @@ -3575,7 +3591,16 @@ function Uninstall-Profile {
)
foreach ($dir in $profileDirs) {
$mainProfile = Join-Path $dir 'Microsoft.PowerShell_profile.ps1'
if (Test-Path $mainProfile) {
$originalBackup = Join-Path $dir 'profile.original.ps1'
if (Test-Path $originalBackup) {
# Restore the pre-install profile saved on first install instead of leaving none behind.
if ($PSCmdlet.ShouldProcess($mainProfile, 'Restore original profile saved before first install')) {
Copy-Item -Path $originalBackup -Destination $mainProfile -Force -ErrorAction SilentlyContinue
Remove-Item $originalBackup -Force -ErrorAction SilentlyContinue
Write-Host " Restored original profile to $mainProfile" -ForegroundColor Green
}
}
elseif (Test-Path $mainProfile) {
if ($PSCmdlet.ShouldProcess($mainProfile, 'Remove profile file')) {
Remove-Item $mainProfile -Force -ErrorAction SilentlyContinue
Write-Host " Removed $mainProfile" -ForegroundColor DarkGray
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ The **install wizard runs by default** — pick theme / scheme / font / features
Inspired by [powerlevel10k](https://github.com/romkatv/powerlevel10k)'s `p10k configure`. **Runs automatically on every interactive install** — it's the default experience, not an opt-in. Non-interactive environments skip it.

```powershell
# Default trusted-download flow — wizard runs as part of installation
# Recommended (review first): run with no hash flag. setup.ps1 downloads the bundle,
# prints its SHA-256 hashes, and stops WITHOUT installing. Review them (ideally against
# the commit list at https://github.com/26zl/PowerShellPerfect/commits/main), then re-run
# pinning the combined hash for a verified install (the wizard runs during install):
$setup = irm "https://github.com/26zl/PowerShellPerfect/raw/main/setup.ps1"
& ([scriptblock]::Create($setup)) # prints hashes, then stops
& ([scriptblock]::Create($setup)) -ExpectedSha256 '<combined-hash>' # verified install

# Unverified fallback: skip the integrity check and install whatever was just downloaded.
& ([scriptblock]::Create($setup)) -SkipHashCheck

# Bypass the wizard and apply repo defaults (Tokyo Night + CascadiaCode):
Expand All @@ -63,6 +70,8 @@ $setup = irm "https://github.com/26zl/PowerShellPerfect/raw/main/setup.ps1"
Reconfigure-Profile
```

> Same guarantee as the Quick Install above: the printed hash proves download integrity, not upstream authenticity (see the note there). A signed-release or tag-pinning model would add authenticity and is left as a maintainer follow-up.

**Steps** (all accept Enter to keep the default / skip):

1. **Quick start** — one-key preset: Tokyo Night scheme, CascadiaCode Nerd Font, VS Code editor, dark chrome, default features. Answer yes to jump straight to the summary.
Expand Down
23 changes: 15 additions & 8 deletions setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ function Save-WizardChoices {
)
$utf8 = [System.Text.UTF8Encoding]::new($false)
$s = if (Test-Path $UserSettingsPath) {
try { (Get-Content $UserSettingsPath -Raw | ConvertFrom-Json -ErrorAction Stop) }
try { (Get-Content $UserSettingsPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop) }
catch { [PSCustomObject]@{} }
}
else { [PSCustomObject]@{} }
Expand Down Expand Up @@ -426,7 +426,7 @@ function Start-InstallWizard {
# Resume from state file if caller passed one that exists
if ($StatePath -and (Test-Path $StatePath)) {
try {
$prev = Get-Content $StatePath -Raw | ConvertFrom-Json
$prev = Get-Content $StatePath -Raw -Encoding UTF8 | ConvertFrom-Json
$prevSchema = if ($prev.PSObject.Properties['schemaVersion']) { [int]$prev.schemaVersion } else { 1 }
if ($prevSchema -ne $WIZARD_STATE_SCHEMA) {
Write-Host ''
Expand Down Expand Up @@ -1107,7 +1107,7 @@ if ($wantWizard) {
try {
$configTmp = Join-Path $env:TEMP ("psp-theme-" + [System.IO.Path]::GetRandomFileName() + ".json")
Copy-Item (Resolve-SetupSourcePath -Kind 'theme') $configTmp -Force -ErrorAction Stop
$profileConfig = Get-Content $configTmp -Raw | ConvertFrom-Json
$profileConfig = Get-Content $configTmp -Raw -Encoding UTF8 | ConvertFrom-Json
Copy-Item $configTmp (Join-Path $configCachePath "theme.json") -Force
Remove-Item $configTmp -ErrorAction SilentlyContinue
}
Expand All @@ -1120,7 +1120,7 @@ $terminalConfig = $null
try {
$terminalConfigTmp = Join-Path $env:TEMP ("psp-terminal-" + [System.IO.Path]::GetRandomFileName() + ".json")
Copy-Item (Resolve-SetupSourcePath -Kind 'terminal') $terminalConfigTmp -Force -ErrorAction Stop
$terminalConfig = Get-Content $terminalConfigTmp -Raw | ConvertFrom-Json
$terminalConfig = Get-Content $terminalConfigTmp -Raw -Encoding UTF8 | ConvertFrom-Json
Copy-Item $terminalConfigTmp (Join-Path $configCachePath "terminal-config.json") -Force
Remove-Item $terminalConfigTmp -ErrorAction SilentlyContinue
}
Expand Down Expand Up @@ -1153,7 +1153,7 @@ $script:WtManaged = $true
$userSettingsPath = Join-Path $configCachePath "user-settings.json"
if (Test-Path $userSettingsPath) {
try {
$userSettings = Get-Content $userSettingsPath -Raw | ConvertFrom-Json
$userSettings = Get-Content $userSettingsPath -Raw -Encoding UTF8 | ConvertFrom-Json
# windowsTerminal.manage = false hands settings.json to another tool, such as a dotfiles repo.
if ($userSettings.windowsTerminal -and $userSettings.windowsTerminal.PSObject.Properties['manage'] -and $userSettings.windowsTerminal.manage -eq $false) { $script:WtManaged = $false }
if ($profileConfig -and $userSettings.theme) {
Expand Down Expand Up @@ -1217,6 +1217,13 @@ foreach ($dir in $profileDirs) {
$tempDownload = Join-Path $env:TEMP ("psp-profile_download_" + (Split-Path $dir -Leaf) + "_" + [System.IO.Path]::GetRandomFileName() + ".ps1")
Copy-Item (Resolve-SetupSourcePath -Kind 'profile') $tempDownload -Force -ErrorAction Stop
if (Test-Path -Path $targetProfile -PathType Leaf) {
# Preserve the pre-install profile once as an immutable copy that rotation never deletes
# and Uninstall-Profile restores; oldprofile* rotation below never matches this name.
$originalBackup = Join-Path $dir 'profile.original.ps1'
if (-not (Test-Path -Path $originalBackup)) {
Copy-Item -Path $targetProfile -Destination $originalBackup -Force
Write-Host " Original profile saved to [$originalBackup]" -ForegroundColor DarkGray
}
# Keep the latest five timestamped profile backups.
$backupStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backupPath = Join-Path $dir ("oldprofile.$backupStamp.ps1")
Expand Down Expand Up @@ -1402,13 +1409,13 @@ function Install-OhMyPoshTheme {
$alreadyValid = $false
if (Test-Path -LiteralPath $themeFilePath -PathType Leaf) {
try {
$null = Get-Content $themeFilePath -Raw -ErrorAction Stop | ConvertFrom-Json
$null = Get-Content $themeFilePath -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json
$alreadyValid = $true
} catch { $null = $_ }
}
if (-not $alreadyValid) {
Invoke-DownloadWithRetry -Uri $ThemeUrl -OutFile $themeFilePath
$null = Get-Content $themeFilePath -Raw | ConvertFrom-Json
$null = Get-Content $themeFilePath -Raw -Encoding UTF8 | ConvertFrom-Json
Write-Host " Theme '$ThemeName' downloaded." -ForegroundColor Green
}
else {
Expand Down Expand Up @@ -1606,7 +1613,7 @@ foreach ($wtSettingsPath in $wtSettingsPaths) {
$wt = $null
for ($wtAttempt = 1; $wtAttempt -le 2; $wtAttempt++) {
try {
$wtRaw = (Get-Content $wtSettingsPath -Raw) -replace $jsoncCommentPattern, ''
$wtRaw = (Get-Content $wtSettingsPath -Raw -Encoding UTF8) -replace $jsoncCommentPattern, ''
$wt = $wtRaw | ConvertFrom-Json
break
}
Expand Down
Loading