#!/usr/bin/env pwsh <# .SYNOPSIS Windows equivalent of ./link-dotfiles (the GNU stow script). .DESCRIPTION stow can't run natively on Windows, so this mirrors what it does: home_root/* -> ~\ (e.g. ~\.zshrc) home_config/* -> ~\.config\ (e.g. ~\.config\nvim) Directories are linked with junctions and files with symlinks/hardlinks, none of which need administrator rights (junctions and hardlinks never do; symlinks are attempted first in case Developer Mode is on, then we fall back). Re-running is safe: correct links are skipped, and any real file or directory that would be clobbered is moved aside to .bak first. .EXAMPLE pwsh -File .\link-dotfiles.ps1 #> [CmdletBinding()] param( # Print what would happen without changing anything. [switch]$DryRun ) $ErrorActionPreference = 'Stop' $repo = $PSScriptRoot $home_ = $HOME # stow metadata files we must never link into the home tree. $skip = @('.stow-local-ignore', '.stow-global-ignore') function Get-LinkTarget { param([string]$Path) $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue if ($item -and $item.LinkType) { return $item.Target } return $null } function Link-Item { param( [string]$Source, # absolute path inside the repo [string]$Dest # absolute path under the home tree ) $isDir = (Get-Item -LiteralPath $Source -Force).PSIsContainer # Already linked to the right place? Nothing to do. $existingTarget = Get-LinkTarget $Dest if ($existingTarget -and ($existingTarget -ieq $Source)) { Write-Host " ok $Dest" -ForegroundColor DarkGray return } if (Test-Path -LiteralPath $Dest) { if ($existingTarget) { # A link, but pointing elsewhere -> replace it. if ($DryRun) { Write-Host " relink $Dest (was -> $existingTarget)" -ForegroundColor Yellow; return } Remove-Item -LiteralPath $Dest -Force -Recurse } else { # A real file/dir -> back it up rather than destroy it. $bak = "$Dest.bak" Write-Host " backup $Dest -> $bak" -ForegroundColor Yellow if (-not $DryRun) { Move-Item -LiteralPath $Dest -Destination $bak -Force } } } if ($DryRun) { Write-Host " link $Dest -> $Source" -ForegroundColor Cyan; return } $parent = Split-Path $Dest -Parent if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } if ($isDir) { New-Item -ItemType Junction -Path $Dest -Target $Source | Out-Null Write-Host " junc $Dest -> $Source" -ForegroundColor Green } else { # Prefer a real symlink (needs admin or Developer Mode); fall back to a # hardlink (same volume, no admin); finally just copy. try { New-Item -ItemType SymbolicLink -Path $Dest -Target $Source -ErrorAction Stop | Out-Null Write-Host " link $Dest -> $Source" -ForegroundColor Green } catch { try { New-Item -ItemType HardLink -Path $Dest -Target $Source -ErrorAction Stop | Out-Null Write-Host " hard $Dest -> $Source" -ForegroundColor Green } catch { Copy-Item -LiteralPath $Source -Destination $Dest -Force Write-Host " copy $Dest (could not link; copied)" -ForegroundColor Magenta } } } } Write-Host "home_root -> $home_" Get-ChildItem -LiteralPath (Join-Path $repo 'home_root') -Force | Where-Object { $skip -notcontains $_.Name } | ForEach-Object { Link-Item $_.FullName (Join-Path $home_ $_.Name) } $configDir = Join-Path $home_ '.config' Write-Host "home_config -> $configDir" Get-ChildItem -LiteralPath (Join-Path $repo 'home_config') -Force | Where-Object { $skip -notcontains $_.Name } | ForEach-Object { Link-Item $_.FullName (Join-Path $configDir $_.Name) } # Windows Terminal lives in a package folder outside the stow tree, so link its # settings.json explicitly. Skipped silently if Terminal isn't installed. $wtPkg = Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Packages') -Filter 'Microsoft.WindowsTerminal_*' -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 $wtSrc = Join-Path $repo 'windows\windows-terminal\settings.json' if ($wtPkg -and (Test-Path $wtSrc)) { $wtDest = Join-Path $wtPkg.FullName 'LocalState\settings.json' Write-Host "windows-terminal -> $wtDest" Link-Item $wtSrc $wtDest } Write-Host "Done." -ForegroundColor Green