Files
vs-code-setup/scripts/settings.ps1
T
maxandClaude Fable 5.1 9085266dd3 Scripts work under pwsh on Linux and macOS
Home, VS Code user folder and CLI are resolved per platform (with
VSCODE_USER_DIR as an override); dev-link makes a symlink where a
junction is not available.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
2026-09-08 19:30:43 +02:00

193 lines
6.9 KiB
PowerShell

<#
.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