Files
vs-code-setup/README.md
T
maxandClaude Fable 5.1 c058c09050 Bash install.sh and settings.sh for Linux and macOS
Twins of the PowerShell scripts, needing only bash, curl and one of jq,
node or python3 for JSON. install.sh downloads a release (or installs
from dist/), settings.sh pushes, pulls or diffs User/, both resolve the
code CLI and user folder per platform.

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

9.5 KiB

VS Code setup

Builds three local VS Code extensions, publishes them to a Gitea release, installs them from there, and keeps user settings in git.

Extension What it does
colored-references Find All References in a syntax-highlighted virtual document, plus a sortable results panel
vertical-tabs A vertical list of open tabs, colour-coded by project, with pinning
dotnet-solution-launcher Startup project and solution configuration (Test | x64) in the status bar; build and debug that pass the platform DotRush drops

The short version

# once per machine, on the machine that builds
$env:GITEA_TOKEN = '<token>'
.\scripts\publish.ps1 -Build          # package all three, upload to the "latest" release

# on any machine, including a fresh one
.\scripts\install.ps1                 # download from the release and install
.\scripts\settings.ps1 -Pull          # apply settings, keybindings, marketplace extensions

# the same on Linux or macOS, no PowerShell needed
scripts/install.sh
scripts/settings.sh --pull

Set the server first — either in config.json or with GITEA_URL, GITEA_OWNER and GITEA_REPO. The repo named there is where releases go; it does not have to be this repo.

Why not Settings Sync

VS Code's built-in Settings Sync cannot be pointed at Gitea. It signs in with a Microsoft or GitHub account and talks to Microsoft's own sync service; there is no setting for a different backend, self-hosted or otherwise. The old Gist-based Settings Sync extension is out too, because Gitea has no gists.

So settings live in User/ as ordinary files, and scripts/settings.ps1 copies them in either direction. That is less magic than Settings Sync and more legible: a diff shows what changed.

It copies rather than symlinks on purpose. A symlink at 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 stops without anything looking broken.

Extensions from the marketplace are handled separately: settings.ps1 -Push records them in User/extensions.txt, -Pull installs the missing ones. Ids starting local. are skipped, because those are the three above and they come from the release.

Scripts

Script Does
build.ps1 Packages each extension into dist/*.vsix. -Clone fetches missing sources from Gitea, -Pull updates them, -Only <id> narrows it
publish.ps1 Creates the release if needed and uploads dist/*.vsix. -Build builds first, -Tag picks the tag
install.ps1 Downloads a release's assets and installs them. -Local installs from dist/ instead, -Uninstall removes them
settings.ps1 -Push machine to repo, -Pull repo to machine, -Diff compares
dev-link.ps1 Junctions the sources into ~/.vscode/extensions for development. -Unlink undoes it
ci-release.sh The bash equivalent of publish.ps1, for the Gitea Actions runner. SKIP_PACKAGE=1 uploads dist/ without rebuilding
install.sh The bash equivalent of install.ps1: --tag, --local, --only, --uninstall, --token
settings.sh The bash equivalent of settings.ps1: --push, --pull, --diff, --no-extensions

Every PowerShell script takes -? for its full help, the bash ones --help.

Tags

publish.ps1 defaults to a rolling latest release: publishing again replaces the assets in place rather than adding duplicates, so install.ps1 with no arguments always gets current builds. Use -Tag v0.2.0 for a snapshot you want to keep, and install.ps1 -Tag v0.2.0 to go back to it.

The tag itself is created on the default branch the first time. For a release that only carries binaries the commit it points at does not mean much, but it is why latest keeps pointing at an old commit — the assets are what move.

Tokens

Never in this repo. publish.ps1 and install.ps1 read, in order:

  1. -Token <value>
  2. $env:GITEA_TOKEN
  3. ~/.gitea-token (%USERPROFILE%\.gitea-token on Windows)

Create one at <gitea>/user/settings/applications with write:repository scope. install.ps1 only reads, so a read:repository token is enough there — and if the release repo is public it needs no token at all for the download, though the API call that finds the release still wants one on most instances.

Building on Node 18

vsce needs Node 20 or newer. On Node 18 it fails with ReferenceError: File is not defined, from a transitive undici, and pinning an older vsce does not help because undici still resolves to a current version.

vsce/package.json works around it by pinning both, and build.ps1 installs into that folder on first run. If you upgrade Node to 20+ you can delete vsce/ and use npx @vscode/vsce package directly.

Upgrading Node is the better fix. The workaround exists so a machine stuck on 18 is not blocked.

Automating it

.gitea/workflows/release.yml builds and publishes on a v* tag push, or on demand. It needs:

  • a registered act_runner on the instance
  • a GITEA_TOKEN secret with write:repository, and read access to the extension repos
  • the actions/checkout and actions/setup-node actions reachable — Gitea pulls those from github.com unless the instance sets [actions] DEFAULT_ACTIONS_URL

The runner uses Node 20, so CI sidesteps the problem above entirely.

ci-release.sh needs only bash, git, node and curl — JSON goes through node, not jq, since node is required anyway and jq frequently is not.

Why not tea

Gitea's CLI (tea) can do this — tea release create and tea release assets create cover the upload. It is not used here because these scripts then need nothing but PowerShell and code: no Go binary to install per machine, no tea login add step, no second place a token lives. Downloading assets on the install side is also plainer over the API than through tea.

If you already have tea set up, tea release create --tag latest --asset dist/*.vsix is a fine substitute for publish.ps1.

Layout

config.json                 Gitea server + the extensions and where they live
User/                       settings.json, keybindings.json, snippets/, extensions.txt
dist/                       built .vsix files (ignored)
scripts/                    the scripts above
vsce/                       pinned vsce, for Node 18 (node_modules ignored)
.gitea/workflows/           the release workflow

config.json paths are relative to this repo, so it expects the extension folders as siblings. Move them and it is one edit.

Tests

.\test\run.ps1

Runs the real scripts against test/fake-gitea.js, a stub of the release endpoints, with a stub code on PATH so nothing is installed into the editor. It checks the things that fail quietly:

  • the .vsix survives the upload byte for byte — it is a zip, so any text-encoding step in the multipart body would corrupt it and still look like a success
  • the token reaches the API, and survives the redirect on the download
  • republishing a tag replaces its assets instead of accumulating duplicates
  • every install passes --force
  • build.ps1 and install.ps1 -Local work with no Gitea configured at all
  • ci-release.sh publishes the same set as publish.ps1

Two bugs came out of writing it. MultipartFormDataContent.Add(content, name, fileName) emits an unquoted name=attachment on .NET Framework, which Gitea's Go parser accepts but nothing guarantees — the header is now set explicitly. And Invoke-WebRequest drops the Authorization header when it follows a redirect, so downloading a private repo's asset returned 401; Save-GiteaAsset follows redirects itself, re-sending the token only while the host does not change.

What it does not cover: a real Gitea instance, and whether VS Code loads the packages once installed. Both were checked by hand.

Known limitations

  • No auto-update. VS Code only offers updates for marketplace extensions, so a new release does not notify you. Re-run install.ps1.
  • --force on every install, because VS Code otherwise declines to reinstall a version it already has, which would silently do nothing whenever you republish without bumping the version.
  • A private extension gallery — pointing VS Code's product.json at your own gallery so updates work normally — is possible but gets overwritten by VS Code updates. Not worth it for three extensions.
  • dev-link.ps1 and install.ps1 conflict. A junctioned source and an installed .vsix are two copies of the same extension id; VS Code loads one arbitrarily. dev-link.ps1 -Unlink before installing.
  • Linux and macOS. install.sh and settings.sh are bash twins of the PowerShell scripts and need only bash, curl and one of jq, node or python3 for JSON. The .ps1 scripts also run there under PowerShell 7 (pwsh). Both find VS Code's user folder at ~/.config/Code/User (~/Library/Application Support/Code/User on macOS) and the CLI in the usual apt, snap, flatpak and Homebrew locations. Set VSCODE_USER_DIR for Insiders or a portable install. dev-link.ps1 makes a symlink instead of a junction. ci-release.sh needs only bash.