Answers two things: how to install these without a marketplace, and how to keep settings in sync when VS Code's Settings Sync cannot be pointed at Gitea. - build.ps1 packages each extension into dist/ - publish.ps1 creates a release and uploads them, replacing same-named assets so a rolling "latest" tag stays clean - install.ps1 downloads a release's assets and installs them, or -Local from dist/ - settings.ps1 copies User/ between the repo and the machine, and installs the marketplace extensions recorded in extensions.txt - dev-link.ps1 junctions the sources into ~/.vscode/extensions for development - ci-release.sh plus a Gitea Actions workflow do the build and publish on a tag push Settings are copied, not symlinked: an atomic save replaces a symlink with a regular file and the sync stops without looking broken. vsce/ pins vsce and undici so packaging works on Node 18, which current vsce does not support. test/run.ps1 drives the real scripts against a stub of the Gitea release API. It caught the multipart Content-Disposition being unquoted on .NET Framework, and Invoke-WebRequest dropping the Authorization header across a redirect.
193 lines
6.9 KiB
PowerShell
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 = Join-Path $env:APPDATA 'Code\User'
|
|
|
|
if (-not (Test-Path $codeUser)) {
|
|
throw ("VS Code's user folder was not found at $codeUser. If you use Insiders or a " +
|
|
"portable install, point APPDATA at 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
|