<# .SYNOPSIS Moves VS Code user settings between this repo and the machine. .DESCRIPTION -Push copies the machine's settings into User/ so you can commit them. -Pull copies User/ onto the machine, backing up whatever was there. -Diff says what differs without touching anything. This copies rather than symlinking on purpose. A symlink at %APPDATA%\Code\User\settings.json is fragile: an editor that saves atomically (write a temp file, rename it over the target) replaces the link with a regular file, and the sync silently stops without anything looking wrong. Copying is explicit -- you can see in git what changed and when. -Push also records your marketplace extensions in User/extensions.txt, and -Pull installs any that are missing. Ids starting "local." are skipped: those are the extensions in this repo, and install.ps1 gets them from the Gitea release. .EXAMPLE .\scripts\settings.ps1 -Diff .\scripts\settings.ps1 -Push .\scripts\settings.ps1 -Pull #> [CmdletBinding(DefaultParameterSetName = 'Diff')] param( [Parameter(ParameterSetName = 'Push', Mandatory)] [switch] $Push, [Parameter(ParameterSetName = 'Pull', Mandatory)] [switch] $Pull, [Parameter(ParameterSetName = 'Diff')] [switch] $Diff, [switch] $NoExtensions ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'common.ps1') $repoUser = Join-Path (Get-RepoRoot) 'User' $codeUser = Get-CodeUserDir if (-not (Test-Path $codeUser)) { throw ("VS Code's user folder was not found at $codeUser. If you use Insiders or a " + "portable install, set VSCODE_USER_DIR to it or copy by hand.") } # Only the things worth version-controlling. Everything else in User/ is state: # globalStorage, workspaceStorage, History, sync, logs. $items = @( @{ Name = 'settings.json'; Kind = 'File' }, @{ Name = 'keybindings.json'; Kind = 'File' }, @{ Name = 'tasks.json'; Kind = 'File' }, @{ Name = 'snippets'; Kind = 'Directory' } ) function Get-Fingerprint { param([string] $Path, [string] $Kind) if (-not (Test-Path $Path)) { return $null } if ($Kind -eq 'File') { return (Get-FileHash $Path -Algorithm SHA256).Hash } $parts = Get-ChildItem -Path $Path -Recurse -File | Sort-Object FullName | ForEach-Object { "$($_.FullName.Substring($Path.Length)):$((Get-FileHash $_.FullName -Algorithm SHA256).Hash)" } if (-not $parts) { return $null } $joined = $parts -join '|' $bytes = [System.Text.Encoding]::UTF8.GetBytes($joined) $sha = [System.Security.Cryptography.SHA256]::Create() try { ([BitConverter]::ToString($sha.ComputeHash($bytes))) -replace '-', '' } finally { $sha.Dispose() } } function Copy-Item2 { param([string] $From, [string] $To, [string] $Kind) $parent = Split-Path -Parent $To if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } if ($Kind -eq 'File') { Copy-Item -Path $From -Destination $To -Force } else { if (Test-Path $To) { Remove-Item -Path $To -Recurse -Force } Copy-Item -Path $From -Destination $To -Recurse -Force } } function Backup-Existing { param([string] $Path, [string] $Kind) if (-not (Test-Path $Path)) { return } $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $backup = "$Path.backup-$stamp" if ($Kind -eq 'File') { Copy-Item -Path $Path -Destination $backup -Force } else { Copy-Item -Path $Path -Destination $backup -Recurse -Force } Write-Host " backed up to $(Split-Path -Leaf $backup)" -ForegroundColor DarkGray } $mode = $PSCmdlet.ParameterSetName Write-Host "repo: $repoUser" Write-Host "code: $codeUser" Write-Host '' foreach ($item in $items) { $inRepo = Join-Path $repoUser $item.Name $onDisk = Join-Path $codeUser $item.Name $repoPrint = Get-Fingerprint -Path $inRepo -Kind $item.Kind $diskPrint = Get-Fingerprint -Path $onDisk -Kind $item.Kind if (-not $repoPrint -and -not $diskPrint) { Write-Host " $($item.Name): absent both sides" -ForegroundColor DarkGray continue } if ($repoPrint -eq $diskPrint) { Write-Host " $($item.Name): identical" -ForegroundColor DarkGray continue } switch ($mode) { 'Push' { if (-not $diskPrint) { Write-Host " $($item.Name): not on this machine, leaving the repo copy alone" ` -ForegroundColor Yellow continue } Copy-Item2 -From $onDisk -To $inRepo -Kind $item.Kind Write-Host " $($item.Name): machine -> repo" -ForegroundColor Green } 'Pull' { if (-not $repoPrint) { Write-Host " $($item.Name): not in the repo, leaving the machine alone" ` -ForegroundColor Yellow continue } Backup-Existing -Path $onDisk -Kind $item.Kind Copy-Item2 -From $inRepo -To $onDisk -Kind $item.Kind Write-Host " $($item.Name): repo -> machine" -ForegroundColor Green } default { $where = if (-not $repoPrint) { 'only on this machine' } elseif (-not $diskPrint) { 'only in the repo' } else { 'differs' } Write-Host " $($item.Name): $where" -ForegroundColor Yellow } } } if ($NoExtensions) { return } Write-Host '' $listPath = Join-Path $repoUser 'extensions.txt' $code = Get-CodeCommand if ($mode -eq 'Push') { $installed = @(& $code --list-extensions) | Where-Object { $_ -and $_ -notlike 'local.*' } | Sort-Object if (-not (Test-Path $repoUser)) { New-Item -ItemType Directory -Path $repoUser | Out-Null } Set-Content -Path $listPath -Value $installed -Encoding UTF8 Write-Host " extensions.txt: recorded $($installed.Count) marketplace extension(s)" ` -ForegroundColor Green return } if (-not (Test-Path $listPath)) { Write-Host ' extensions.txt: not in the repo yet, run -Push on your main machine' ` -ForegroundColor Yellow return } $wanted = @(Get-Content $listPath | Where-Object { $_ -and -not $_.StartsWith('#') }) $present = @(& $code --list-extensions) $missing = @($wanted | Where-Object { $present -notcontains $_ }) if ($mode -eq 'Diff') { if ($missing.Count -eq 0) { Write-Host " extensions.txt: all $($wanted.Count) present" -ForegroundColor DarkGray } else { Write-Host " extensions.txt: $($missing.Count) missing: $($missing -join ', ')" ` -ForegroundColor Yellow } return } if ($missing.Count -eq 0) { Write-Host " extensions.txt: all $($wanted.Count) already installed" -ForegroundColor DarkGray return } foreach ($id in $missing) { Write-Host " installing $id" -ForegroundColor DarkGray & $code --install-extension $id } Write-Host '' Write-Host 'Reload the window to pick up new settings and extensions.' -ForegroundColor Cyan