Public/Repair-MSIXDesktop7Shortcut.ps1
|
function Repair-MSIXDesktop7Shortcut { <# .SYNOPSIS Relocates desktop7:Shortcut entries from the all-users Start Menu ([{Common Programs}]) to the per-user Start Menu ([{Programs}]). .DESCRIPTION A per-user MSIX install does not populate the all-users Start Menu (C:\ProgramData\...\Start Menu\Programs), so [{Common Programs}] shortcuts never appear. [{Programs}] (per-user) does. This moves both the manifest File token and the physical .lnk; Icon, Arguments and Description are kept unchanged (an Icon pointing at an .exe is valid per the desktop7 schema). .PARAMETER MSIXFolderPath Expanded MSIX package folder. Pipeline by property name. .PARAMETER File File token of a specific shortcut. Omit to relocate every [{Common Programs}] one. .EXAMPLE Get-MSIXDesktop7Shortcut -MSIXFolder $pkg | Repair-MSIXDesktop7Shortcut .NOTES Tim Mangan: https://www.tmurgent.com/TmBlog/?p=3857 Andreas Nick, 2026 #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [System.IO.DirectoryInfo] $MSIXFolderPath, [Parameter(ValueFromPipelineByPropertyName = $true, Position = 1)] [string] $File ) begin { # Keyed by folder -> list of File tokens passed via pipeline (empty = all) $pending = @{} $fromToken = '[{Common Programs}]' $toToken = '[{Programs}]' } process { $key = $MSIXFolderPath.FullName if (-not $pending.ContainsKey($key)) { $pending[$key] = [System.Collections.Generic.List[string]]::new() } if (-not [string]::IsNullOrEmpty($File)) { $pending[$key].Add($File) } } end { foreach ($folder in $pending.Keys) { $manifestPath = Join-Path $folder 'AppxManifest.xml' if (-not (Test-Path $manifestPath)) { Write-Warning "AppxManifest.xml not found in '$folder' - skipping." continue } $manifest = New-Object System.Xml.XmlDocument $manifest.PreserveWhitespace = $false $manifest.Load($manifestPath) $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable) $AppXNamespaces.GetEnumerator() | ForEach-Object { $null = $nsmgr.AddNamespace($_.Key, $_.Value) } $targets = $pending[$folder] # empty = all $changed = $false foreach ($sc in @($manifest.SelectNodes('//desktop7:Shortcut', $nsmgr))) { $fileTok = $sc.GetAttribute('File') if (-not $fileTok.StartsWith($fromToken)) { continue } if ($targets.Count -gt 0 -and $targets -notcontains $fileTok) { continue } $newFileTok = $toToken + $fileTok.Substring($fromToken.Length) if (-not $PSCmdlet.ShouldProcess($folder, "Relocate shortcut '$fileTok' -> '$newFileTok'")) { continue } # Move the physical .lnk to match the new token's VFS folder. $oldOnDisk = Resolve-MSIXShortcutOnDiskPath -FolderPath $folder -Relative (Resolve-MSIXShortcutTokenPath -TokenPath $fileTok) $newRel = Resolve-MSIXShortcutTokenPath -TokenPath $newFileTok if ($oldOnDisk -and $newRel) { # Mirror the package's encoding (makeappx = spaces, raw ZIP = %20) if ($oldOnDisk -like '*%20*') { $newRel = $newRel -replace ' ', '%20' } $newOnDisk = Join-Path $folder $newRel $newDir = Split-Path $newOnDisk -Parent if (-not (Test-Path $newDir)) { $null = New-Item -ItemType Directory -Path $newDir -Force } Move-Item -LiteralPath $oldOnDisk -Destination $newOnDisk -Force Write-Verbose "Moved .lnk -> $newRel" } else { Write-Warning "Physical .lnk for '$fileTok' not found - updating manifest only." } $null = $sc.SetAttribute('File', $newFileTok) $changed = $true Write-Verbose "Relocated '$fileTok' -> '$newFileTok'." } if ($changed) { $manifest.Save($manifestPath) Write-Verbose "Saved $manifestPath" } else { Write-Warning "No [{Common Programs}] shortcuts to relocate in '$folder'." } } } } |