Files
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

191 lines
9.5 KiB
Markdown

# 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
```powershell
# 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`](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/`](User/) as ordinary files, and
[`scripts/settings.ps1`](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`](scripts/build.ps1) | Packages each extension into `dist/*.vsix`. `-Clone` fetches missing sources from Gitea, `-Pull` updates them, `-Only <id>` narrows it |
| [`publish.ps1`](scripts/publish.ps1) | Creates the release if needed and uploads `dist/*.vsix`. `-Build` builds first, `-Tag` picks the tag |
| [`install.ps1`](scripts/install.ps1) | Downloads a release's assets and installs them. `-Local` installs from `dist/` instead, `-Uninstall` removes them |
| [`settings.ps1`](scripts/settings.ps1) | `-Push` machine to repo, `-Pull` repo to machine, `-Diff` compares |
| [`dev-link.ps1`](scripts/dev-link.ps1) | Junctions the sources into `~/.vscode/extensions` for development. `-Unlink` undoes it |
| [`ci-release.sh`](scripts/ci-release.sh) | The bash equivalent of `publish.ps1`, for the Gitea Actions runner. `SKIP_PACKAGE=1` uploads `dist/` without rebuilding |
| [`install.sh`](scripts/install.sh) | The bash equivalent of `install.ps1`: `--tag`, `--local`, `--only`, `--uninstall`, `--token` |
| [`settings.sh`](scripts/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`](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`](.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`](https://gitea.com/gitea/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
```powershell
.\test\run.ps1
```
Runs the real scripts against [`test/fake-gitea.js`](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.