diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 712fa25..71a3ed9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -158,6 +158,40 @@ jobs:
Compress-Archive -Path "$packageDir/*" -DestinationPath "$artifacts/SqlPilot-$tag.zip"
+ # ─── Modern .vsix for gallery distribution (additive — payload ZIP above is untouched) ───
+ # A .vsix is an OPC ZIP of the SSMS22 layout + [Content_Types].xml.
+ # Built from a SEPARATE staging copy so the loose-file SSMS22/ folder
+ # shipped in the payload ZIP stays byte-for-byte identical.
+ $vsixStaging = "$artifacts/vsix-staging"
+ New-Item -ItemType Directory -Path $vsixStaging -Force | Out-Null
+ Copy-Item "$ssms22Dir/*" $vsixStaging -Recurse -Force
+ # Drop the loose-file deployment metadata from the .vsix only. catalog.json/
+ # manifest.json make the modern SSMS 22 VSIXInstaller take a v3-catalog path
+ # that rejects the manifest ("version Invalid"); a clean OPC vsix (manifest +
+ # payload) installs. The payload ZIP above still ships them for the installer.
+ Remove-Item "$vsixStaging/catalog.json","$vsixStaging/manifest.json" -Force -ErrorAction SilentlyContinue
+ # Build the content as an indented array-join (NOT a column-0 here-string,
+ # which would break this YAML literal block). Single-quoted lines keep the
+ # XML angle brackets/quotes literal.
+ $contentTypes = @(
+ ''
+ ''
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ''
+ ) -join "`r`n"
+ # Brackets in the filename are PowerShell wildcards — must use -LiteralPath.
+ Set-Content -LiteralPath "$vsixStaging/[Content_Types].xml" -Value $contentTypes -Encoding utf8 -NoNewline
+ $vsixPath = "$artifacts/SqlPilot-$tag.vsix"
+ Remove-Item $vsixPath -Force -ErrorAction SilentlyContinue
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ [System.IO.Compression.ZipFile]::CreateFromDirectory($vsixStaging, $vsixPath)
+ Write-Host "Built VSIX: $vsixPath"
+ Get-ChildItem $vsixStaging | Select-Object Name
+
# ─── Friendly WPF installer (separate ZIP) ───
# SqlPilotInstaller.exe + its dependency DLLs. End users download
# this small ZIP, extract it, and run the .exe — the installer
@@ -177,7 +211,36 @@ jobs:
with:
files: |
artifacts/SqlPilot-*.zip
+ artifacts/SqlPilot-*.vsix
artifacts/SqlPilotInstaller-*.zip
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref, '-') }}
+
+ - name: Publish to extension galleries
+ # Skip prerelease tags (containing '-') so test tags don't hit the galleries.
+ if: ${{ !contains(github.ref, '-') }}
+ shell: pwsh
+ env:
+ VSIXGALLERY_TOKEN: ${{ secrets.VSIXGALLERY_TOKEN }}
+ SSMSGALLERY_TOKEN: ${{ secrets.SSMSGALLERY_TOKEN }}
+ run: |
+ $tag = "${{ steps.version.outputs.tag }}"
+ $vsix = "artifacts/SqlPilot-$tag.vsix"
+ if (-not (Test-Path $vsix)) { Write-Error "VSIX not found: $vsix"; exit 1 }
+ $repo = "https://github.com/mourier/sql-pilot"
+ $issues = "https://github.com/mourier/sql-pilot/issues"
+ $readme = "https://raw.githubusercontent.com/mourier/sql-pilot/main/README.md"
+
+ # VSIX Gallery (vsixgallery.com) — raw body + X-Manage-Token header.
+ $vg = "https://www.vsixgallery.com/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme"
+ Write-Host "Publishing to VSIX Gallery..."
+ curl.exe --fail --silent --show-error -X POST $vg -H "X-Manage-Token: $env:VSIXGALLERY_TOKEN" --data-binary "@$vsix"
+ if ($LASTEXITCODE -ne 0) { throw "VSIX Gallery publish failed with exit code $LASTEXITCODE" }
+
+ # SSMS Gallery (ssmsgallery.azurewebsites.net) — multipart form per its devguide.
+ $sg = "https://ssmsgallery.azurewebsites.net/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme"
+ Write-Host "Publishing to SSMS Gallery..."
+ curl.exe --fail --silent --show-error -X POST $sg -H "X-Manage-Token: $env:SSMSGALLERY_TOKEN" -F "file=@$vsix"
+ if ($LASTEXITCODE -ne 0) { throw "SSMS Gallery publish failed with exit code $LASTEXITCODE" }
+ Write-Host "Gallery publish complete."
diff --git a/README.md b/README.md
index b50790f..e2681e9 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@
+
+
+
@@ -91,6 +94,10 @@ The installer downloads the matching release payload from GitHub itself, verifie
> **First run will show "Windows protected your PC"** because we don't pay for an EV code-signing certificate yet. Click **More info** → **Run anyway**. The installer source is in [`src/SqlPilot.Installer/`](src/SqlPilot.Installer) — verify before running if you want.
+### SSMS 22 via VSIX Gallery (alternative)
+
+SSMS 22 users can also install from the [VSIX Gallery](https://www.vsixgallery.com/extension/SqlPilot.8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b/) or [SSMS Gallery](https://ssmsgallery.azurewebsites.net/): download the `.vsix` and run it (or subscribe to the gallery feed for update notifications). **SSMS 18 and 20 must use the installer above** — the `.vsix` targets the modern shell (SSMS 21–22) only.
+
### Power user — scripted install
For CI pipelines, silent installs, or admins who script everything:
diff --git a/build/SqlPilotDlls.txt b/build/SqlPilotDlls.txt
index d739f87..3cadf05 100644
--- a/build/SqlPilotDlls.txt
+++ b/build/SqlPilotDlls.txt
@@ -3,11 +3,8 @@ SqlPilot.Core.dll
SqlPilot.Smo.dll
SqlPilot.UI.dll
CommunityToolkit.Mvvm.dll
-System.Text.Json.dll
-System.Text.Encodings.Web.dll
System.Buffers.dll
System.Memory.dll
System.Numerics.Vectors.dll
System.Runtime.CompilerServices.Unsafe.dll
System.Threading.Tasks.Extensions.dll
-System.ValueTuple.dll
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index fddcb07..96de992 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -229,13 +229,22 @@ C:\Program Files (x86)\Microsoft SQL Server Management Studio {18|20}\Common7\ID
## Release Pipeline
-A tag push matching `v*` triggers `.github/workflows/release.yml`, which builds Modern + Legacy + the WPF installer in one `msbuild SqlPilot.sln` step and publishes **two release assets**:
+A tag push matching `v*` triggers `.github/workflows/release.yml`, which builds Modern + Legacy + the WPF installer in one `msbuild SqlPilot.sln` step and publishes **three release assets**:
- **`SqlPilot-vX.Y.Z.zip`** — the payload. Contains `SSMS22/` and `SSMS18-20/` subfolders with the extension DLLs + manifests + pkgdef, plus `Install-SqlPilot.ps1`, `Uninstall-SqlPilot.ps1`, `_SsmsHelpers.ps1`, LICENSE, NOTICE, README at the ZIP root. This is the file the WPF installer downloads at runtime, and what power users extract for scripted installs.
- **`SqlPilotInstaller-vX.Y.Z.zip`** — the friendly user-facing installer. Contains `SqlPilotInstaller.exe` + its dependency DLLs. Users download this, extract, double-click the exe, approve UAC, and the installer fetches the matching payload ZIP from GitHub itself.
+- **`SqlPilot-vX.Y.Z.vsix`** — the modern (SSMS 21–22) gallery artifact. See *Modern `.vsix` artifact* below.
The installer pins to its own assembly version: it asks GitHub for `releases/tags/v{Major.Minor.Build}`, not `/releases/latest`. This keeps a `v1.0.0` installer from accidentally pulling a `v2.0.0` payload.
+### Modern `.vsix` artifact
+
+`release.yml` also produces `SqlPilot-vX.Y.Z.vsix` as a *purely additive* output. A `.vsix` is just an OPC ZIP of the modern extension layout, so the workflow takes a **separate copy** of the already-assembled `SSMS22/` folder (DLLs from `build/SqlPilotDlls.txt` + `pkgdef` + `extension.vsixmanifest` (v2) + `manifest.json` + `catalog.json`), drops in a generated `[Content_Types].xml`, and zips it via `System.IO.Compression`. Because it reuses the same assembled folder, payload parity with the installer is automatic. The loose-file payload ZIP is built from its own folder and stays byte-for-byte unchanged — the `.vsix` is never substituted for it.
+
+The `.vsix` is attached to the GitHub release and, for non-prerelease tags only (tags without a `-` suffix), pushed to the [VSIX Gallery](https://www.vsixgallery.com/) and [SSMS Gallery](https://ssmsgallery.azurewebsites.net/). Manage tokens come from the repo secrets `VSIXGALLERY_TOKEN` and `SSMSGALLERY_TOKEN`; both galleries key the listing off the manifest `Identity Id`, so re-publishing updates the same entry.
+
+**Version-target policy:** the modern manifest targets SSMS `[21.0,23.0)` (SSMS 21–22). Keep the upper bound *pinned* — bump it per release only after testing on the new SSMS shell. Do **not** open-end the range.
+
### Version substitution
The version string `1.0.0` is hardcoded in **seven** files because we don't build a proper VSIX (no VSSDK packaging step — `dotnet build` doesn't produce one). The release workflow regex-patches all seven before calling `msbuild`:
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index e83706f..54eefea 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -63,6 +63,10 @@ For unattended deployment:
Per-version errors don't abort the others — if SSMS 20 is locked because it's still running, the uninstaller still cleans up 18 and 22 and tells you which versions to retry.
+## SSMS 22 via VSIX Gallery (alternative)
+
+SSMS 22 users can also install from the [VSIX Gallery](https://www.vsixgallery.com/extension/SqlPilot.8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b/) or [SSMS Gallery](https://ssmsgallery.azurewebsites.net/): download the `.vsix` and run it (or subscribe to the gallery feed for update notifications). **SSMS 18 and 20 must use the installer or scripts above** — the `.vsix` targets the modern shell (SSMS 21–22) only.
+
## Manual install (last resort)
If both the WPF installer and the PowerShell scripts are unavailable for some reason, you can copy the files by hand. The release ZIP contains the exact file set under `SSMS22/` and `SSMS18-20/`.
diff --git a/docs/SSMS_INTEGRATION_NOTES.md b/docs/SSMS_INTEGRATION_NOTES.md
index bfb82c3..7b60423 100644
--- a/docs/SSMS_INTEGRATION_NOTES.md
+++ b/docs/SSMS_INTEGRATION_NOTES.md
@@ -357,3 +357,5 @@ For user-visible messages, use the SQL Pilot status bar (`IndexStatus.Text`).
- **`SynchronizeTree` without pre-expanded tree** — triggers connection dialogs
- **Building against SSMS 22's SMO and expecting it to work on SSMS 18** — version mismatch at runtime
- **Passing a `nodeInfo` from `FindNode` to `OpenTableHelperClass.EditTopNRows` without setting `Connection.DatabaseName` first** — triggers a new connection dialog because the connection defaults to master/empty
+- **`true` on `SqlPilot.Package.csproj`** to get a `.vsix` out of the build — produces **no** `.vsix`. The VSSDK `CreateVsixContainer` target never imports because `source.extension.vsixmanifest` is only auto-globbed as a plain `None` item in this SDK-style project; the property is read but no target runs (verified via `-v:diagnostic`). Instead, the release builds the `.vsix` by zipping a copy of the assembled `SSMS22/` release folder plus a generated `[Content_Types].xml` (an OPC ZIP), which guarantees payload parity with the installer via the shared `build/SqlPilotDlls.txt`. See `release.yml` and `docs/ARCHITECTURE.md` § Modern `.vsix` artifact.
+- **Shipping `catalog.json` / `manifest.json` *inside* the `.vsix`** — the modern SSMS 22 VSIXInstaller (the VS-Installer ServiceHub `VSIXInstaller.exe`) reads the metadata fine but **fails the actual install** with `InvalidExtensionManifestException: the provided vsixmanifest, version Invalid is not vsixV1, vsixV2, or vsixV3`. Their presence makes it take a v3-catalog install path that rejects the manifest. A clean OPC vsix containing only `extension.vsixmanifest` + `[Content_Types].xml` + payload (DLLs + pkgdef) installs with exit code 0 (to `%LOCALAPPDATA%\Microsoft\SSMS\\Extensions\\`). So `release.yml` deletes `catalog.json`/`manifest.json` from the `.vsix` staging copy only — the loose-file payload ZIP still ships them for the file-copy installer. (`catalog.json`/`manifest.json` are the VS2017+ loose-file deployment descriptors, not VSIX-container files.)
diff --git a/src/SqlPilot.Package/manifest.json b/src/SqlPilot.Package/manifest.json
index 40dd2ea..3302620 100644
--- a/src/SqlPilot.Package/manifest.json
+++ b/src/SqlPilot.Package/manifest.json
@@ -12,7 +12,11 @@
{"fileName": "/SqlPilot.UI.dll", "sha256": null},
{"fileName": "/SqlPilot.Package.pkgdef", "sha256": null},
{"fileName": "/CommunityToolkit.Mvvm.dll", "sha256": null},
- {"fileName": "/System.Text.Json.dll", "sha256": null},
+ {"fileName": "/System.Buffers.dll", "sha256": null},
+ {"fileName": "/System.Memory.dll", "sha256": null},
+ {"fileName": "/System.Numerics.Vectors.dll", "sha256": null},
+ {"fileName": "/System.Runtime.CompilerServices.Unsafe.dll", "sha256": null},
+ {"fileName": "/System.Threading.Tasks.Extensions.dll", "sha256": null},
{"fileName": "/manifest.json", "sha256": null},
{"fileName": "/catalog.json", "sha256": null}
],