Skip to content

Commit 23e5e3f

Browse files
committed
fix: make oh-my-posh tool lifecycle MSI-aware
1 parent d6937b6 commit 23e5e3f

4 files changed

Lines changed: 132 additions & 25 deletions

File tree

Microsoft.PowerShell_profile.ps1

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,20 @@ function Get-ExternalCommandPath {
113113

114114
$cmd = Get-Command $CommandName -ErrorAction SilentlyContinue
115115
if (-not $cmd) { return $null }
116-
if ($cmd.Path) { return $cmd.Path }
117-
if ($cmd.Source) { return $cmd.Source }
118-
if ($cmd.Definition) { return $cmd.Definition }
116+
117+
if ($cmd.CommandType -eq 'Alias' -and $cmd.Definition -and $cmd.Definition -ne $CommandName) {
118+
return Get-ExternalCommandPath -CommandName $cmd.Definition
119+
}
120+
121+
$pathCandidates = @($cmd.Path, $cmd.Source, $cmd.Definition) |
122+
Where-Object { $_ -and [System.IO.Path]::IsPathRooted([string]$_) } |
123+
Select-Object -Unique
124+
foreach ($pathCandidate in $pathCandidates) {
125+
if (Test-Path -LiteralPath $pathCandidate -PathType Leaf) {
126+
return $pathCandidate
127+
}
128+
}
129+
119130
return $null
120131
}
121132

@@ -140,11 +151,6 @@ function Merge-JsonObject {
140151

141152
# Specific helper to get the path to oh-my-posh executable for cache clearing (since it has a built-in cache clear command instead of a file-based cache)
142153
function Get-OhMyPoshExecutablePath {
143-
$resolvedPath = Get-ExternalCommandPath -CommandName 'oh-my-posh'
144-
if ($resolvedPath) {
145-
return $resolvedPath
146-
}
147-
148154
$candidatePaths = @(
149155
(Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe'),
150156
(Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\oh-my-posh.exe'),
@@ -156,6 +162,12 @@ function Get-OhMyPoshExecutablePath {
156162
$candidatePaths += (Join-Path $pf86 'oh-my-posh\bin\oh-my-posh.exe')
157163
}
158164

165+
$resolvedPath = Get-ExternalCommandPath -CommandName 'oh-my-posh'
166+
$windowsAppsRoot = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps'
167+
if ($resolvedPath -and $resolvedPath -notlike "$windowsAppsRoot*") {
168+
return $resolvedPath
169+
}
170+
159171
foreach ($candidatePath in ($candidatePaths | Select-Object -Unique)) {
160172
if (-not (Test-Path -LiteralPath $candidatePath)) { continue }
161173

@@ -189,6 +201,47 @@ function Get-OhMyPoshInstallInfo {
189201
}
190202
}
191203

204+
function Test-WingetPackageInstalled {
205+
param(
206+
[Parameter(Mandatory)]
207+
[string]$Id
208+
)
209+
210+
$wingetPath = Get-ExternalCommandPath -CommandName 'winget'
211+
if (-not $wingetPath) { return $false }
212+
213+
try {
214+
$wingetOutput = @(& $wingetPath list --id $Id --exact 2>&1)
215+
$wingetText = (@($wingetOutput) -join [Environment]::NewLine).Trim()
216+
if ($LASTEXITCODE -ne 0 -or -not $wingetText) { return $false }
217+
if ($wingetText -match 'No installed package found') { return $false }
218+
return $wingetText -match [regex]::Escape($Id)
219+
}
220+
catch {
221+
return $false
222+
}
223+
}
224+
225+
function Get-OhMyPoshMsiProductCode {
226+
$roots = @(
227+
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
228+
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
229+
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
230+
)
231+
232+
$entries = Get-ItemProperty -Path $roots -ErrorAction SilentlyContinue |
233+
Where-Object { $_.DisplayName -eq 'Oh My Posh' }
234+
foreach ($entry in $entries) {
235+
foreach ($uninstallString in @($entry.QuietUninstallString, $entry.UninstallString)) {
236+
if ($uninstallString -and $uninstallString -match '\{[0-9A-Fa-f\-]{36}\}') {
237+
return $Matches[0]
238+
}
239+
}
240+
}
241+
242+
return $null
243+
}
244+
192245
# Resolve executable path for a profile tool (OMP uses Get-OhMyPoshInstallInfo, others use Get-Command)
193246
function Get-ProfileToolExecutablePath {
194247
param(
@@ -2274,17 +2327,49 @@ function Uninstall-Profile {
22742327
}
22752328
}
22762329

2277-
# Phase 4: Winget tools (opt-in)
2278-
if ($RemoveTools -and (Get-Command winget -ErrorAction SilentlyContinue)) {
2330+
# Phase 4: Managed tools (winget by default, direct/MSI aware for Oh My Posh)
2331+
if ($RemoveTools) {
22792332
if ($isCiOrAgent) {
22802333
Write-Host ' Skipping managed tool uninstall under CI/agent environment.' -ForegroundColor DarkGray
22812334
}
22822335
else {
2336+
$wingetPath = Get-ExternalCommandPath -CommandName 'winget'
22832337
foreach ($tool in $script:ProfileTools) {
2284-
if (Get-Command $tool.Cmd -ErrorAction SilentlyContinue) {
2338+
$toolPath = Get-ProfileToolExecutablePath -Tool $tool
2339+
$wingetManaged = if ($wingetPath) { Test-WingetPackageInstalled -Id $tool.Id } else { $false }
2340+
$removalMode = $null
2341+
$ompMsiProductCode = $null
2342+
2343+
if ($tool.Cmd -eq 'oh-my-posh') {
2344+
if ($wingetManaged) {
2345+
$removalMode = 'winget'
2346+
}
2347+
else {
2348+
$ompMsiProductCode = Get-OhMyPoshMsiProductCode
2349+
}
2350+
2351+
if ($ompMsiProductCode) {
2352+
$removalMode = 'msi'
2353+
}
2354+
}
2355+
elseif ($wingetManaged) {
2356+
$removalMode = 'winget'
2357+
}
2358+
2359+
if (-not $removalMode) {
2360+
if ($toolPath) {
2361+
$toolLocation = if (-not $wingetPath -and $tool.Cmd -ne 'oh-my-posh') { 'install present but winget is unavailable' }
2362+
elseif ($tool.Cmd -eq 'oh-my-posh') { 'direct/MSI install without winget registration' }
2363+
else { 'local install without winget registration' }
2364+
Write-Host " Preserving $($tool.Name) ($toolLocation)." -ForegroundColor DarkGray
2365+
}
2366+
continue
2367+
}
2368+
2369+
if ($removalMode -eq 'winget') {
22852370
if ($PSCmdlet.ShouldProcess($tool.Name, 'Uninstall via winget')) {
22862371
try {
2287-
$wingetOutput = @(& winget uninstall -e --id $tool.Id --silent 2>&1)
2372+
$wingetOutput = @(& $wingetPath uninstall -e --id $tool.Id --silent 2>&1)
22882373
if ($LASTEXITCODE -eq 0) {
22892374
Write-Host " Uninstalled $($tool.Name)" -ForegroundColor Green
22902375
}
@@ -2298,14 +2383,34 @@ function Uninstall-Profile {
22982383
Write-Warning " Failed to uninstall $($tool.Name): $_"
22992384
}
23002385
}
2386+
continue
2387+
}
2388+
2389+
if ($removalMode -eq 'msi') {
2390+
if (-not $ompMsiProductCode) {
2391+
Write-Warning ' Could not locate an MSI product code for Oh My Posh.'
2392+
continue
2393+
}
2394+
2395+
if ($PSCmdlet.ShouldProcess($tool.Name, 'Uninstall via MSI')) {
2396+
try {
2397+
$msiProc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/x', $ompMsiProductCode, '/qn', '/norestart') -Wait -PassThru -WindowStyle Hidden
2398+
if ($msiProc.ExitCode -in @(0, 1605, 3010)) {
2399+
Write-Host " Uninstalled $($tool.Name)" -ForegroundColor Green
2400+
}
2401+
else {
2402+
Write-Warning " Failed to uninstall $($tool.Name) via MSI (exit $($msiProc.ExitCode))."
2403+
}
2404+
}
2405+
catch {
2406+
Write-Warning " Failed to uninstall $($tool.Name) via MSI: $_"
2407+
}
2408+
}
23012409
}
23022410
}
23032411
}
23042412
}
23052413
elseif (-not $RemoveTools) { $preserved += 'Managed tools (use -RemoveTools to uninstall)' }
2306-
elseif (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
2307-
Write-Warning ' winget not found - managed tools were not removed.'
2308-
}
23092414

23102415
# Phase 5: Nerd Fonts (opt-in, requires admin)
23112416
if ($RemoveFonts) {

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ irm "https://github.com/26zl/PowerShellPerfect/raw/main/setup.ps1" | iex
2626

2727
The terminal restarts automatically when setup finishes (new tab in Windows Terminal, or new window otherwise). For the best experience use [PowerShell 7](https://github.com/PowerShell/PowerShell).
2828

29-
> **Recommended for Oh My Posh:** Install the x64 MSI release manually instead of relying on `winget`/Store (`WindowsApps`/MSIX). This repo preserves a direct MSI install and avoids the WindowsApps path when possible.
30-
31-
If Oh My Posh is already installed directly via MSI, setup preserves that install instead of forcing it back through the WindowsApps/MSIX path.
29+
> **Recommended for Oh My Posh:** Install the x64 MSI from the [releases](https://github.com/JanDeDobbeleer/oh-my-posh/releases) page (see [Oh My Posh](https://github.com/JanDeDobbeleer/oh-my-posh)) instead of `winget`/Store—this profile preserves a direct install and avoids the WindowsApps path. If you already have the MSI install, setup leaves it as is.
3230
3331
### Manual Setup
3432

@@ -68,12 +66,12 @@ Remove the profile, caches, and Windows Terminal changes:
6866

6967
```powershell
7068
Uninstall-Profile # Core cleanup: profile files, caches, WT restore, PSFzf
71-
Uninstall-Profile -RemoveTools # Also uninstall managed CLI tools (Oh My Posh, eza, etc.)
69+
Uninstall-Profile -RemoveTools # Also uninstall managed CLI tools (including direct/MSI Oh My Posh when detected)
7270
Uninstall-Profile -All # Remove everything including tools, fonts, and user data
7371
Uninstall-Profile -All -HardResetWindowsTerminal # Same as -All, but also delete WT settings.json so WT recreates factory defaults
7472
```
7573

76-
Optional switches: `-RemoveTools` (winget packages), `-RemoveUserData` (profile_user.ps1, user-settings.json), `-RemoveFonts` (Nerd Fonts, requires admin), `-All` (everything), `-HardResetWindowsTerminal` (delete WT settings.json and backups so Windows Terminal recreates defaults). Supports `-WhatIf` to preview without making changes.
74+
Optional switches: `-RemoveTools` (winget-managed tools plus direct/MSI Oh My Posh when registered as MSI), `-RemoveUserData` (profile_user.ps1, user-settings.json), `-RemoveFonts` (Nerd Fonts, requires admin), `-All` (everything), `-HardResetWindowsTerminal` (delete WT settings.json and backups so Windows Terminal recreates defaults). Supports `-WhatIf` to preview without making changes.
7775

7876
## Customization
7977

ci-functional.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,9 +780,11 @@ Invoke-TestCase -Name 'Coverage audit against profile exports' -Code {
780780
$internalOnly = @(
781781
'Get-ExternalCommandPath'
782782
'Get-OhMyPoshInstallInfo'
783+
'Get-OhMyPoshMsiProductCode'
783784
'Get-OhMyPoshExecutablePath'
784785
'Get-ProfileToolExecutablePath'
785786
'Get-ProfileToolVersionText'
787+
'Test-WingetPackageInstalled'
786788
'Invoke-OhMyPoshCommand'
787789
'Get-OhMyPoshPromptContext'
788790
'Get-OhMyPoshPromptText'

setup.ps1

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,6 @@ function Invoke-DownloadWithRetry {
188188

189189
# Resolve oh-my-posh executable path (Get-Command or known install locations)
190190
function Get-OhMyPoshExecutablePath {
191-
$cmd = Get-Command oh-my-posh -ErrorAction SilentlyContinue
192-
if ($cmd -and $cmd.Path) {
193-
return $cmd.Path
194-
}
195-
196191
$candidatePaths = @(
197192
(Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe'),
198193
(Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\oh-my-posh.exe'),
@@ -204,6 +199,13 @@ function Get-OhMyPoshExecutablePath {
204199
$candidatePaths += (Join-Path $pf86 'oh-my-posh\bin\oh-my-posh.exe')
205200
}
206201

202+
$cmd = Get-Command oh-my-posh -ErrorAction SilentlyContinue
203+
$resolvedPath = if ($cmd -and $cmd.Path -and (Test-Path -LiteralPath $cmd.Path -PathType Leaf)) { $cmd.Path } else { $null }
204+
$windowsAppsRoot = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps'
205+
if ($resolvedPath -and $resolvedPath -notlike "$windowsAppsRoot*") {
206+
return $resolvedPath
207+
}
208+
207209
foreach ($candidatePath in ($candidatePaths | Select-Object -Unique)) {
208210
if (-not (Test-Path -LiteralPath $candidatePath)) { continue }
209211

0 commit comments

Comments
 (0)