Build, publish to Gitea releases, and install the three extensions
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.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<#
|
||||
.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
|
||||
Reference in New Issue
Block a user