From e4a297d5b27b7057be3c93b0df0ebbd017fb0415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Spo=CC=88nemann?= Date: Fri, 22 May 2026 09:29:55 +0200 Subject: [PATCH] Add publish-vscode-extension skill Covers preparing and publishing a VS Code extension to both the VS Code Marketplace and the Open VSX Registry, with reference files for the package.json manifest, bundling with esbuild/webpack, and each registry's authentication and publisher/namespace model. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/publish-vscode-extension/SKILL.md | 237 ++++++++++++++++++ .../references/bundling.md | 215 ++++++++++++++++ .../references/manifest.md | 134 ++++++++++ .../references/open-vsx.md | 151 +++++++++++ .../references/vscode-marketplace.md | 119 +++++++++ 5 files changed, 856 insertions(+) create mode 100644 skills/publish-vscode-extension/SKILL.md create mode 100644 skills/publish-vscode-extension/references/bundling.md create mode 100644 skills/publish-vscode-extension/references/manifest.md create mode 100644 skills/publish-vscode-extension/references/open-vsx.md create mode 100644 skills/publish-vscode-extension/references/vscode-marketplace.md diff --git a/skills/publish-vscode-extension/SKILL.md b/skills/publish-vscode-extension/SKILL.md new file mode 100644 index 0000000..8b29857 --- /dev/null +++ b/skills/publish-vscode-extension/SKILL.md @@ -0,0 +1,237 @@ +--- +name: publish-vscode-extension +description: Prepare and publish a VS Code extension to the VS Code Marketplace and/or the Open VSX Registry. Use whenever the user wants to publish, ship, release, or re-publish a VS Code extension; build or upload a `.vsix`; set up `vsce` or `ovsx`; create a marketplace publisher or an Open VSX namespace; configure CI for extension releases; bump versions for an extension; or troubleshoot publishing errors. Covers both registries because most production extensions ship to both — invoke even if the user names only one of them. +--- + +# Publish a VS Code extension + +There are two registries where VS Code extensions live: + +- **VS Code Marketplace** — Microsoft's registry. Consumed by VS Code, Visual Studio, and Azure DevOps. Authenticated with an Azure DevOps Personal Access Token (PAT). Tooling: `@vscode/vsce`. +- **Open VSX Registry** — the Eclipse Foundation's open-source registry at https://open-vsx.org. Consumed by VS Codium, Gitpod, Theia-based IDEs, Cursor, Windsurf, code-server, and most non-Microsoft forks. Authenticated with a token issued by open-vsx.org after accepting the Eclipse Publisher Agreement. Tooling: `ovsx`. + +Most production extensions publish to **both**. The same `.vsix` file works on either registry, so the recommended flow is *package once with `vsce`, then publish that artifact to each registry*. Don't fall into publishing only to Marketplace — non-Microsoft VS Code distributions can't see those extensions. + +When more context is needed, load: + +- `references/manifest.md` — the full `package.json` extension manifest: every required, recommended, and runtime field, the allowed `categories` list, marketplace presentation fields (`galleryBanner`, `preview`, `badges`, `pricing`, `sponsor`, `qna`), runtime fields (`main`, `browser`, `activationEvents`, `contributes`, `capabilities`, `extensionKind`), dependency fields (`extensionDependencies`, `extensionPack`), and validation gotchas. Read whenever you're editing `package.json`, troubleshooting "field X was rejected" errors, or auditing an unfamiliar extension's manifest. +- `references/vscode-marketplace.md` — Azure DevOps PAT, publisher creation, `vsce` commands, verified publisher, unpublishing, common 401/403 errors. Read whenever a step involves Marketplace authentication, publisher identity, or `vsce`-specific flags. +- `references/open-vsx.md` — Eclipse account setup, the Publisher Agreement, access tokens, namespace ownership and the "unverified" warning, `ovsx` commands, CI integration. Read whenever a step involves Open VSX, namespaces, or `ovsx`. +- `references/bundling.md` — why and how to bundle the extension with esbuild or webpack, wiring into `vscode:prepublish`, externalizing the `vscode` module, Web Extension targets, `.vscodeignore` for bundled extensions. Read whenever the extension isn't bundled yet, ships to Web hosts (`vscode.dev`, `github.dev`), is unexpectedly large, or has a slow activation time. + +## Decide what to publish to + +If the user hasn't said, default to **both** registries and tell them. The marginal cost of also publishing to Open VSX is small (one token + one namespace) and the audience reached is large. Only skip Open VSX if the extension explicitly depends on proprietary Marketplace-only APIs or telemetry that wouldn't be appropriate for open-source distributions — and even then, say so explicitly. + +## Prerequisites that apply to both registries + +Install Node.js, then install the publishing CLIs: + +```bash +npm install -g @vscode/vsce ovsx +``` + +`vsce` (the "VS Code Extension manager") is the canonical packager. `ovsx` reuses `vsce` internally for packaging-from-source, so installing both is the normal setup. `npx @vscode/vsce` / `npx ovsx` also work if you'd rather not install globally. + +## Prepare the extension + +Before publishing anywhere, make sure the extension is publish-ready. These are the same requirements for both registries — the registries reject or downrank extensions that fail them. + +### `package.json` manifest + +A minimal publish-ready manifest looks like: + +```json +{ + "name": "extension-name", + "displayName": "Human Readable Name", + "description": "One sentence about what this extension does", + "version": "0.0.1", + "publisher": "", + "engines": { "vscode": "^1.84.0" }, + "categories": ["Other"], + "keywords": ["tag1", "tag2"], + "icon": "icon.png", + "repository": { "type": "git", "url": "https://github.com/user/repo" }, + "license": "MIT", + "bugs": { "url": "https://github.com/user/repo/issues" }, + "homepage": "https://github.com/user/repo#readme" +} +``` + +Four required fields: `name`, `version`, `publisher`, `engines.vscode`. Everything else is technically optional but the registry listing will look broken without `displayName`, `description`, `categories`, `icon`, `repository`, and `license`. + +Two cross-cutting things to know up front: + +- `publisher` is the same string on both registries by convention, but they're two independent identity systems — registering `acme` on Marketplace does *not* reserve `acme` on Open VSX, and vice versa. Register both names early to avoid squatters. +- `engines.vscode` must be a real published VS Code version range (e.g. `^1.84.0`). Wildcards (`*`) are rejected. + +For the full field reference — allowed `categories` values, marketplace presentation fields (`galleryBanner`, `preview`, `badges`, `pricing`, `sponsor`), runtime fields (`main`, `browser`, `activationEvents`, `contributes`, `capabilities`, `extensionKind`), dependency fields (`extensionDependencies`, `extensionPack`), and the validation gotchas (icon constraints, 30-keyword limit, `repository` object vs string, etc.) — read `references/manifest.md`. + +### Supporting files + +- `README.md` — rendered as the extension's marketplace landing page on both registries. Images in it **must use absolute HTTPS URLs**, not relative paths (the registries serve the README from a different origin). +- `LICENSE` — required for unambiguous licensing. Match the `license` field in `package.json` (SPDX identifier like `MIT`, `Apache-2.0`). +- `CHANGELOG.md` — rendered as the "Changelog" tab. Conventional format is Keep-a-Changelog. +- `.vscodeignore` — exclude files from the `.vsix` (glob per line). Source TypeScript, tests, configs, and `node_modules/` dev deps should not ship. Example: + ``` + .vscode/** + .vscode-test/** + src/** + .gitignore + .yarnrc + vsc-extension-quickstart.md + **/tsconfig.json + **/.eslintrc.json + **/*.map + **/*.ts + !out/**/*.js + ``` + `vsce` automatically excludes `devDependencies` declared in `package.json`, so you don't need to list them. + +## Package once + +Build the `.vsix` from the extension root: + +```bash +vsce package +``` + +This runs the `vscode:prepublish` script (typically a TypeScript or bundler build), validates the manifest, and produces `-.vsix`. Inspect the file list with `vsce ls` before publishing the first time — finding shipped `node_modules/` or `.git/` directories on the marketplace is embarrassing and hard to undo. + +If the extension isn't bundled, `vsce package` will warn and the resulting `.vsix` typically ships the full `node_modules/` tree — large, slow to activate, and **unusable in Web hosts like `vscode.dev` and `github.dev`**. Bundling is strongly recommended before the first publish; see `references/bundling.md` for esbuild and webpack setup. + +### Recommended workflow: package once, publish twice + +When publishing to both registries, **first build the `.vsix` with `vsce package`, then upload that exact same file to each registry** — do not call `vsce publish` and `ovsx publish` against the source tree separately. Reasons this matters: + +- **Byte-identical artifact on both registries.** Users get the same hash whether they install from Marketplace or Open VSX, which is what they expect and what reproducibility / supply-chain audits require. Two from-source publishes can produce subtly different zips (timestamps, file ordering, dev-dep resolution at a different second), and you have no easy way to spot the divergence after the fact. +- **One artifact, one version, one git tag.** The thing you tagged in git, the thing in a release attachment, and the thing on each registry are all literally the same file. +- **Inspectable before either registry sees it.** Run `vsce ls` or `unzip -l -.vsix` on the artifact, fix anything wrong, and only then push to the registries. +- **Half the build cost in CI.** One bundler invocation instead of two. + +The concrete commands are in the next section; the shape of the recommended flow is: + +```bash +vsce package # produces -.vsix +vsce publish --packagePath -.vsix # → Marketplace +ovsx publish -.vsix # → Open VSX, same file +``` + +Both CLIs do also support package-and-publish-in-one (`vsce publish`, `ovsx publish` with no path argument) — use those only when publishing to a single registry. For dual-registry publishes, always go through a shared `.vsix`. + +## Handling access tokens + +Publishing requires two long-lived bearer credentials — a **Marketplace PAT** issued by Azure DevOps and an **Open VSX token** issued by open-vsx.org. Both are equivalent in power to an npm publish token: anyone holding one can push code that runs on every install of every extension under that identity. The agent's handling of these values is the security boundary, so treat this section as a precondition for every publish step below. + +**If the user pastes a token directly into the prompt** — stop before running anything. The token is now in the conversation transcript, which may be persisted, logged, or replayed. Tell the user to: + +1. Revoke that token immediately: + - **Marketplace PAT** → https://dev.azure.com → avatar → **User settings** → **Personal access tokens** → revoke. + - **Open VSX token** → https://open-vsx.org → avatar → **Settings** → **Access Tokens** → revoke. +2. Mint a new one and provide it via the harness's secret-handling mechanism if one exists, or otherwise via an environment variable in the shell that launches the agent: + - `export VSCE_PAT=...` for Marketplace. + - `export OVSX_PAT=...` for Open VSX. + +Do not proceed using the pasted value. + +**Otherwise**, expect the tokens to live in environment variables and let the CLIs pick them up by name. Both tools already read these names natively, so the command line should contain neither the token value nor an explicit `-p` flag with the variable substituted in: + +- `vsce` reads `VSCE_PAT` automatically; just run `vsce publish …`. +- `ovsx` reads `OVSX_PAT` automatically; just run `ovsx publish …`. + +If the user wants to keep different names (e.g., for a multi-publisher setup), reference them by name (`-p "$MY_PAT"`), never by value. Do not echo, log, write to a file, paste into a commit, or include the value in the summary reported back to the user. + +Before running anything that needs a token, confirm the variable is actually set in the same shell that will run the tool — `[ -n "$VSCE_PAT" ]` and `[ -n "$OVSX_PAT" ]`, **do not print the value**. A token exported in another terminal won't be visible here. If a variable is missing, ask the user to set it; do not ask them to paste it. + +If the agent harness has a built-in secret/credential mechanism (an injected env var, a secret-resolution step, etc.), prefer that over a plain shell variable. + +The `vsce login` / `ovsx login` flows store tokens in the OS keychain after an interactive prompt. That path is fine when the **user** is running the CLI on their own machine — the value never enters the chat. It is **not** appropriate when the agent is doing the publishing, because the agent can't usefully interact with a `Password:` prompt and shouldn't be feeding the value into one anyway. Use the env-var path instead. + +## Publish + +With `VSCE_PAT` and `OVSX_PAT` set in the environment (see above) and following the "package once, publish twice" recommendation: + +```bash +# build a single .vsix +vsce package + +# publish that file to each registry — tokens come from the env vars +vsce publish --packagePath -.vsix +ovsx publish -.vsix +``` + +Both commands accept the same `.vsix` because the registries treat it as an opaque artifact; the manifest inside the zip identifies which `.@` slot it belongs to on each side. + +## Versioning + +Both registries enforce monotonically increasing SemVer per extension. You cannot republish the same version — bump first. + +`vsce` can do the bump-commit-tag for you on a clean git tree: + +```bash +vsce publish patch # 1.0.0 → 1.0.1 +vsce publish minor # 1.0.0 → 1.1.0 +vsce publish major # 1.0.0 → 2.0.0 +vsce publish 1.5.3 # explicit version +vsce publish minor -m "Release v%s" # custom commit message +``` + +`ovsx` doesn't have a version-bumping mode — let `vsce` handle the bump, then point `ovsx` at the resulting `.vsix`. + +## Pre-release versions + +Both registries support a pre-release channel that's installed only when the user opts in (or runs VS Code Insiders): + +```bash +vsce package --pre-release +vsce publish --pre-release +ovsx publish --pre-release +``` + +Requires `engines.vscode >= 1.63.0`. The convention is **odd minor numbers for pre-release, even minor numbers for release** (e.g. `0.3.*` pre-release, `0.4.*` release) — this avoids the pre-release and stable channels colliding when SemVer compares them. + +## Platform-specific extensions + +If the extension ships native binaries (a debug adapter, a language server in Rust/Go, native node modules), build and publish one `.vsix` per platform: + +```bash +vsce package --target win32-x64 +vsce package --target darwin-arm64 +# … repeat for each target, then publish each artifact + +vsce publish --packagePath ./pkgs/*.vsix +ovsx publish ./pkgs/*.vsix +``` + +Available targets: `win32-x64`, `win32-arm64`, `linux-x64`, `linux-arm64`, `linux-armhf`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`, `web`. A `.vsix` without `--target` is treated as universal and installable everywhere. + +## Typical CI release flow + +The community-standard GitHub Action is [`HaaLeo/publish-vscode-extension`](https://github.com/HaaLeo/publish-vscode-extension), which wraps both `vsce` and `ovsx`. A minimal release job: + +```yaml +- run: npm ci +- run: npx vsce package + # produces -.vsix +- uses: HaaLeo/publish-vscode-extension@v1 + with: + pat: ${{ secrets.VSCE_PAT }} + registryUrl: https://marketplace.visualstudio.com + extensionFile: ./-.vsix +- uses: HaaLeo/publish-vscode-extension@v1 + with: + pat: ${{ secrets.OVSX_PAT }} + extensionFile: ./-.vsix + # registryUrl defaults to https://open-vsx.org +``` + +Trigger on tag push (`v*`) rather than every merge, so the version bump is intentional. + +## What to do next + +- Editing `package.json` or hitting a "field X was rejected" validation error? Read `references/manifest.md`. +- Going to authenticate or troubleshoot a Marketplace publish? Read `references/vscode-marketplace.md`. +- Going to register an Open VSX namespace, deal with the unverified-publisher warning, or set up an Eclipse account? Read `references/open-vsx.md`. +- Extension isn't bundled yet, or needs Web Extension support? Read `references/bundling.md` before the next publish. +- Don't know whether the user has identity on either registry yet? Ask before issuing token-creation steps — registering a publisher is a permanent ID claim, and they may already have one under a different account. diff --git a/skills/publish-vscode-extension/references/bundling.md b/skills/publish-vscode-extension/references/bundling.md new file mode 100644 index 0000000..e3d54a0 --- /dev/null +++ b/skills/publish-vscode-extension/references/bundling.md @@ -0,0 +1,215 @@ +# Bundling a VS Code extension + +Bundling collapses the extension's source files and its `node_modules` runtime dependencies into a single JavaScript file (typically `dist/extension.js`). It is not the same step as TypeScript compilation — `tsc` turns TS into JS but doesn't merge files or resolve dependencies, so a non-bundled extension still ships its full `node_modules` tree in the `.vsix`. + +Three reasons it matters for publishing: + +1. **Web Extensions require it.** VS Code for Web (`vscode.dev`, `github.dev`) and Theia/Gitpod web hosts can only load extensions whose entry point is a single bundled file. An unbundled extension ships fine to Marketplace/Open VSX but silently fails to activate in any browser-hosted host. If "Web" is in the extension's `categories` or the manifest declares `"browser"`, bundling is a hard requirement. +2. **Activation latency.** VS Code loads the extension's main file synchronously during activation. Loading one ~200 KB bundle is consistently 5-10× faster than walking 100+ small files in `node_modules/`. +3. **`.vsix` size.** Bundling typically cuts the `.vsix` by an order of magnitude because tree-shaking and minification drop unreachable code, and dev-only branches of dependencies disappear. + +If the extension is being published and isn't bundled yet, raise it as a recommendation — especially before the first publish, because adding bundling later is mechanical but changes the on-disk layout (so `.vscodeignore` and `package.json#main` both have to move with it). + +## Pick a bundler + +Two are worth using in 2025+; both are well-supported by `vsce`: + +- **esbuild** — recommended for new extensions. ~10–100× faster builds, simpler config, drops in cleanly. Cost: strips TypeScript types without checking them, so you must run `tsc --noEmit` separately for type safety. +- **webpack** — recommended only when migrating an existing webpack-based extension, or when the extension needs a loader ecosystem feature esbuild doesn't have (e.g. complex asset pipelines). Slower builds, more config surface. + +Rollup and Parcel work but are uncommon in the extension ecosystem; prefer esbuild unless the user already has rollup configured. + +## Wiring into `vsce` + +`vsce package` runs the `vscode:prepublish` npm script before zipping the `.vsix`. The bundler invocation belongs there: + +```json +{ + "main": "./dist/extension.js", + "scripts": { + "vscode:prepublish": "npm run package", + "package": "npm run check-types && node esbuild.js --production", + "check-types": "tsc --noEmit", + "compile": "npm run check-types && node esbuild.js", + "watch": "npm-run-all -p watch:*", + "watch:esbuild": "node esbuild.js --watch", + "watch:tsc": "tsc --noEmit --watch --project tsconfig.json" + } +} +``` + +Three things that catch people out here: + +- **`package.json#main` must point at the bundle output** (e.g. `./dist/extension.js`), not at the TypeScript source or the un-bundled `out/extension.js`. VS Code will load whatever `main` says — if it's the un-bundled path, all that bundling effort is wasted. +- **`vscode:prepublish` runs every time `vsce package` runs**, including in CI. Make it idempotent and don't put `npm install` in it — assume dependencies are already installed. +- **Type checking has to be separate.** esbuild strips types without checking; webpack with `transpileOnly: true` (a common speedup) does the same. Run `tsc --noEmit` as part of `package`/`prepublish` so a broken type doesn't ship. + +## esbuild setup + +```bash +npm install --save-dev esbuild npm-run-all +``` + +`esbuild.js`: + +```javascript +const esbuild = require('esbuild'); + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +async function main() { + const ctx = await esbuild.context({ + entryPoints: ['src/extension.ts'], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/extension.js', + external: ['vscode'], + logLevel: 'warning', + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +main().catch(e => { console.error(e); process.exit(1); }); +``` + +`external: ['vscode']` is **mandatory** — see "Externalizing `vscode`" below. + +For Web Extensions, change `platform: 'node'` to `platform: 'browser'` and ship a second entry under `package.json#browser` pointing at the browser bundle (the Node bundle stays under `main`). + +## webpack setup + +```bash +npm install --save-dev webpack webpack-cli ts-loader +``` + +`webpack.config.js`: + +```javascript +const path = require('path'); + +module.exports = { + target: 'node', // 'webworker' for Web Extensions + entry: './src/extension.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../[resource-path]', + }, + devtool: 'source-map', + externals: { vscode: 'commonjs vscode' }, + resolve: { + mainFields: ['browser', 'module', 'main'], // browser-first for Web targets + extensions: ['.ts', '.js'], + }, + module: { + rules: [{ test: /\.ts$/, exclude: /node_modules/, use: ['ts-loader'] }], + }, +}; +``` + +Scripts: + +```json +{ + "vscode:prepublish": "npm run package", + "package": "webpack --mode production --devtool hidden-source-map", + "compile": "webpack --mode development", + "watch": "webpack --mode development --watch" +} +``` + +`hidden-source-map` keeps stack traces useful internally while not shipping the `.map` to the marketplace. + +## Externalizing the `vscode` module + +The `import vscode from 'vscode'` API is provided by VS Code itself at runtime — there is no `node_modules/vscode/` package with the implementation. The `@types/vscode` package supplies types only. Bundlers must be told to leave `vscode` as an external import: + +- esbuild: `external: ['vscode']` +- webpack: `externals: { vscode: 'commonjs vscode' }` + +Skipping this produces a build error like "Could not resolve 'vscode'" or, worse, accidentally bundles a stale copy that breaks at activation. + +If the extension uses other host-provided modules (rare — `keytar` used to be one), externalize them the same way and add a non-ignored copy under `node_modules/` in `.vscodeignore` (see below). + +## `.vscodeignore` for bundled extensions + +After bundling, the `.vsix` should contain `dist/extension.js`, the manifest, README, LICENSE, and the icon — and basically nothing else. A minimal `.vscodeignore`: + +``` +.vscode/** +.vscode-test/** +.github/** +src/** +out/** +test/** +**/*.ts +**/*.map +**/tsconfig*.json +**/.eslintrc* +**/*.test.js +esbuild.js +webpack.config.js +node_modules/** +``` + +Counter-intuitive part: `node_modules/**` is excluded **because the bundle already contains everything from it**. If an externalized module needs to ship (rare), un-ignore it: `!node_modules/keytar/**`. Verify with `vsce ls` — if you see your dependencies' source trees in the listing, the bundle isn't being picked up. + +## Web Extensions + +If the extension declares `"browser"` in `package.json` or sets `Web` in `categories`, the bundle has to target browser environments: + +- esbuild: `platform: 'browser'`, and avoid Node-only APIs (`fs`, `path`, `child_process`, etc.) in the source. +- webpack: `target: 'webworker'`, plus `resolve.mainFields: ['browser', 'module', 'main']` so npm packages pick their browser-compatible entry points. + +`package.json` ships both entries when an extension supports both desktop and web: + +```json +{ + "main": "./dist/extension-node.js", + "browser": "./dist/extension-web.js" +} +``` + +Web hosts (vscode.dev, github.dev, the Open VSX-fed Theia browser hosts) load `browser`; desktop loads `main`. Build both with separate bundler invocations in `package`/`vscode:prepublish`. + +## Minification and source maps + +- **Minification on**: turn on for production (`minify: production` / `--mode production`). Saves significant bytes. +- **Minification breaks code that relies on `Function.prototype.name`** — class names get mangled. Most VS Code extensions are safe, but if the extension uses reflection or a DI container that looks up types by class name, either disable minification or configure the bundler to keep names (esbuild: `keepNames: true`; webpack: `optimization.minimizer` with `terserOptions: { keep_classnames: true, keep_fnames: true }`). +- **Source maps**: ship hidden source maps in production so stack traces are debuggable but the maps aren't visible to users. esbuild: `sourcemap: true, sourcesContent: false` and exclude the `.map` from the `.vsix` via `.vscodeignore`. webpack: `--devtool hidden-source-map`. + +## Common failures + +| Symptom | Likely cause | Fix | +|---|---|---| +| Extension activates locally but doesn't appear in `vscode.dev` / `github.dev` | Not bundled, or `package.json#browser` missing | Bundle for `browser`/`webworker` target and add the `browser` entry. | +| `Cannot find module 'vscode'` at build time | `vscode` not externalized | Add `external: ['vscode']` (esbuild) or `externals: { vscode: 'commonjs vscode' }` (webpack). | +| Activation throws `Cannot find module ''` after publish | A runtime dep got tree-shaken or wasn't bundled; `node_modules` is ignored in `.vscodeignore` | Either ensure the dep is reachable from `entryPoints`, or externalize it and un-ignore it in `.vscodeignore`. | +| webpack: `Critical dependency: the request of a dependency is an expression` | A `require(variable)` the bundler can't statically resolve | Refactor to a static `require`, or externalize the module and ship it via `.vscodeignore`. | +| `.vsix` is 30 MB+ for a small extension | Bundling not actually running (`vscode:prepublish` not wired up, or `main` still points at unbundled output) | Run `vsce ls` and look for `node_modules/**` in the listing; check `main`. | +| TypeScript type error reached production | Bundler strips types without checking | Add `tsc --noEmit` to the `package`/`vscode:prepublish` script chain. | +| Class-based DI breaks only after publish | Minifier renamed classes | Enable `keepNames` / `keep_classnames`, or disable minification. | +| Icons, snippets, or other static files missing at runtime | Bundler doesn't copy non-JS assets; `.vscodeignore` may exclude them too | Reference assets via `vscode.Uri.joinPath(context.extensionUri, ...)` and ensure those files are NOT excluded by `.vscodeignore`. Bundlers do not copy them by default; either copy via a build step or leave them outside `dist/`. | + +## Inspecting the bundle before publish + +Before the first publish — and any time `.vscodeignore` or the bundler config changes — run: + +```bash +vsce ls # list every file that will be in the .vsix +vsce package # actually build it +unzip -l -.vsix # double-check the zip contents +``` + +If you see `src/`, `node_modules//`, or multiple JS files where there should be one, the bundle isn't being shipped correctly and the next publish will be the slow, oversized one. diff --git a/skills/publish-vscode-extension/references/manifest.md b/skills/publish-vscode-extension/references/manifest.md new file mode 100644 index 0000000..b35894e --- /dev/null +++ b/skills/publish-vscode-extension/references/manifest.md @@ -0,0 +1,134 @@ +# The `package.json` extension manifest + +Every VS Code extension is identified, configured, and described by a single `package.json` file at the extension root — the **manifest**. It's a superset of the standard npm `package.json`: VS Code adds its own fields on top, and rejects or warns on missing ones at publish time. This reference covers the publish-relevant fields. The full schema (every `contributes.*` point, every activation event) lives at https://code.visualstudio.com/api/references/extension-manifest and https://code.visualstudio.com/api/references/contribution-points. + +Both registries (Marketplace and Open VSX) read the same manifest. Field validity is enforced by `vsce package` before either registry sees the file, so manifest mistakes fail fast and locally. + +## Required fields + +These four must be present or `vsce package` refuses to build. + +| Field | Type | Notes | +|---|---|---| +| `name` | string | Lowercase, no spaces; combined with `publisher` to form the global extension ID `.`. Reserved permanently after first publish — choose carefully. | +| `version` | string | Strict SemVer (e.g. `0.1.0`). Each publish must strictly increase this; you cannot republish the same version. | +| `publisher` | string | The Marketplace publisher ID / Open VSX namespace. Same string on both registries by convention but registered independently — see SKILL.md § "Decide what to publish to". | +| `engines.vscode` | string | VS Code version range the extension supports (e.g. `^1.84.0`). Wildcard `*` is rejected. Must be a real released VS Code version. Determines the minimum host the Marketplace will offer the extension to. | + +```json +{ + "name": "my-extension", + "version": "0.1.0", + "publisher": "my-publisher", + "engines": { "vscode": "^1.84.0" } +} +``` + +## Strongly recommended for publishing + +Missing these doesn't block `vsce package`, but the resulting Marketplace/Open VSX listing will look broken or unprofessional. + +| Field | Purpose | Constraints | +|---|---|---| +| `displayName` | Human-readable extension name shown in search and on the listing page. | Should be unique across the marketplace; gets compared to existing entries. | +| `description` | One-line description in search results and at the top of the listing. | Keep under ~150 chars; no marketing fluff or marketplace tags. | +| `categories` | Array used for filtering and discovery. | Pick from the fixed list below — invalid values are silently dropped. | +| `keywords` | Search keywords; do not appear visibly but improve findability. | **Maximum 30 entries** — exceeding this fails the publish. | +| `icon` | Path to the listing icon. | **PNG only**, at least **128×128px** (256×256 recommended for HiDPI). SVG is rejected. Path is relative to the manifest. | +| `repository` | Source code link shown under "Resources". | Object form: `{ "type": "git", "url": "https://github.com/..." }`. String form works but the object form is required for `vsce publish`'s git-tag behavior. | +| `license` | SPDX identifier (`MIT`, `Apache-2.0`, …) matching the `LICENSE` file. | If the license isn't a stock SPDX, use `"SEE LICENSE IN LICENSE.txt"` and ship the file. | +| `bugs` | Issue tracker link shown under "Resources". | `{ "url": "...", "email": "..." }` — email optional. | +| `homepage` | Documentation/project page link shown under "Resources". | Often the README URL on GitHub. | + +### Allowed `categories` values + +Per the official manifest reference: `Programming Languages`, `Snippets`, `Linters`, `Themes`, `Debuggers`, `Formatters`, `Keymaps`, `SCM Providers`, `Other`, `Extension Packs`, `Language Packs`, `Data Science`, `Machine Learning`, `Visualization`, `Notebooks`, `Education`, `Testing`. + +VS Code adds new categories from time to time (recent versions ship `AI`, `Chat`, etc.). If you're publishing an extension that fits a new category, check the linked manifest reference for the current list before assuming an unrecognized value will be honored. + +## Marketplace presentation + +| Field | Purpose | Notes | +|---|---|---| +| `galleryBanner` | Color/theme of the listing page header. | `{ "color": "#C80000", "theme": "dark" }`. `theme` is `"dark"` or `"light"` and controls whether the publisher name renders in light or dark text on the banner. | +| `preview` | Marks the extension as Preview (yellow "Preview" badge on listing). | Boolean. Use for extensions that work but aren't 1.0 yet. Distinct from pre-release versioning (see `--pre-release` flag in SKILL.md). | +| `badges` | Extra status badges on the listing. | Array of `{ "url", "href", "description" }`. The marketplace only allows badges from approved hosts: `shields.io`, `github.com`, `codecov.io`, `snyk.io`, `gitlab.com`, etc. Arbitrary URLs are stripped. | +| `markdown` | README rendering engine. | `"github"` (default) or `"standard"`. Use `"github"` so GitHub-Flavored Markdown features (task lists, tables, code fences with language hints) render the same on the marketplace as on the source README. | +| `qna` | Q&A tab control. | `"marketplace"` (default), a URL string to redirect to your own forum, or `false` to disable the tab entirely. | +| `pricing` | `"Free"` (default) or `"Trial"`. | `"Trial"` requires a separate Microsoft Commercial Marketplace setup; almost all extensions use `"Free"`. | +| `sponsor` | Adds a "Sponsor" link on the listing. | `{ "url": "https://github.com/sponsors/" }`. | + +## Runtime fields + +| Field | Purpose | Notes | +|---|---|---| +| `main` | Entry point for the Node.js extension host (desktop). | Path relative to the manifest, **without `.js`**. After bundling this should point at the bundle output (e.g. `./dist/extension`), not at the un-bundled TypeScript output. See `references/bundling.md`. | +| `browser` | Entry point for the Web Extension host (`vscode.dev`, `github.dev`, web Theia hosts). | Same shape as `main`. Without it, the extension is unavailable in web hosts. | +| `activationEvents` | Array of events that cause VS Code to load the extension. | Examples: `"onLanguage:markdown"`, `"onCommand:myExt.helloWorld"`, `"onStartupFinished"`, `"workspaceContains:**/.mytool"`. VS Code 1.74+ auto-generates events for most contribution points, so this array is usually short. Avoid `"*"` (load-on-startup) — it tanks startup time and is grounds for marketplace rejection of large extensions. | +| `contributes` | Static declarations of commands, menus, keybindings, languages, grammars, themes, settings, views, etc. | Each contribution point has its own schema; see https://code.visualstudio.com/api/references/contribution-points. | +| `capabilities.untrustedWorkspaces` | Declares behavior in restricted-trust workspaces. | `{ "supported": true \| false \| "limited", "description": "..." }`. Required for the extension to load at all in untrusted workspaces; declaring `false` is fine but be honest about it. | +| `capabilities.virtualWorkspaces` | Declares behavior in virtual workspaces (e.g. GitHub repo opened in `vscode.dev`). | Same shape as `untrustedWorkspaces`. | +| `extensionKind` | Where the extension runs in Remote Development setups (SSH, WSL, Containers). | Array of `"ui"` (runs on the local UI side) and/or `"workspace"` (runs in the remote workspace). `["workspace", "ui"]` means "prefer workspace, fall back to UI". Affects nothing on local-only installs. | +| `l10n` | Path to a directory of localization bundles. | Typically `"./l10n"`. Combined with `vscode.l10n` API. | + +## Dependency / packaging fields + +| Field | Purpose | Notes | +|---|---|---| +| `extensionDependencies` | Other extensions that must be installed for this one to work. | Array of `.` IDs. Installing this extension also installs the deps. Use sparingly — it creates install-graph coupling and forks behavior across registries (an Open VSX install can't depend on a Marketplace-only extension). | +| `extensionPack` | Bundles multiple extensions together as a single install. | Array of `.` IDs. Extension Packs must also set `"categories": ["Extension Packs"]`. The pack itself usually has no `main` or code. | +| `dependencies` | npm runtime deps. | Standard npm semantics. Bundled extensions don't ship `node_modules/` — see `references/bundling.md`. | +| `devDependencies` | npm dev deps. | `vsce` auto-excludes these from the `.vsix`, so they don't need to be in `.vscodeignore`. | +| `scripts.vscode:prepublish` | Runs automatically before `vsce package` / `vsce publish`. | Where the build/bundle invocation belongs. Must be idempotent — do not run `npm install` here. | +| `scripts.vscode:uninstall` | Runs when the user uninstalls the extension. | Only Node.js scripts are honored. Use for cleanup (deleting cached files, etc.). Don't rely on it running — if VS Code crashes or the user wipes the extensions folder, it won't fire. | + +## Worked example + +A minimal but publish-ready manifest for a bundled extension that targets both desktop and web: + +```json +{ + "name": "wordcount", + "displayName": "Word Count", + "description": "Reports word count in Markdown files.", + "version": "0.1.0", + "publisher": "ms-vscode", + "engines": { "vscode": "^1.84.0" }, + "categories": ["Other"], + "keywords": ["markdown", "wordcount"], + "icon": "images/icon.png", + "galleryBanner": { "color": "#C80000", "theme": "dark" }, + "license": "MIT", + "main": "./dist/extension-node.js", + "browser": "./dist/extension-web.js", + "activationEvents": ["onLanguage:markdown"], + "contributes": { + "commands": [ + { "command": "wordcount.count", "title": "Word Count: Count selection" } + ] + }, + "capabilities": { + "untrustedWorkspaces": { "supported": true }, + "virtualWorkspaces": { "supported": true } + }, + "scripts": { + "vscode:prepublish": "npm run package", + "package": "npm run check-types && node esbuild.js --production", + "check-types": "tsc --noEmit" + }, + "repository": { "type": "git", "url": "https://github.com/microsoft/vscode-wordcount.git" }, + "bugs": { "url": "https://github.com/microsoft/vscode-wordcount/issues" }, + "homepage": "https://github.com/microsoft/vscode-wordcount#readme" +} +``` + +## Validation gotchas + +- **`repository` as a string** parses fine but breaks `vsce publish `'s automatic git-tag step. Always use the object form. +- **`engines.vscode` not pinned**: a bare `"*"` is rejected; an unrealistically old floor (`^1.0.0`) renders fine but is dishonest — VS Code uses this to gate updates, so pin it at a version you actually test against. +- **`keywords` over 30**: returns `You exceeded the number of allowed tags of 30`. Trim and republish. +- **`icon` is SVG**: rejected at package time. Re-export to PNG ≥ 128×128. +- **`main` still pointing at un-bundled output after adopting a bundler**: extension publishes "successfully" but ships the wrong file. Run `vsce ls` and verify the listing matches what the bundler produced. +- **Missing `browser` entry on a web-relevant extension**: extension installs in `vscode.dev` but never activates. If the extension claims `Web` capability or you want it on browser-hosted IDEs, both `main` and `browser` must be set and the corresponding bundle must exist. +- **`extensionDependencies` to an extension that exists only on Marketplace**: that extension's Open VSX install will fail because the dep can't be resolved. Either remove the hard dep, or publish a forked manifest with the dep removed for Open VSX (rare). +- **Activation event `"*"`** in modern extensions: VS Code 1.74+ auto-generates activation events for most contribution points, so `"*"` is almost always wrong. Replace with the specific `onLanguage:`, `onCommand:`, `onView:`, etc. event. diff --git a/skills/publish-vscode-extension/references/open-vsx.md b/skills/publish-vscode-extension/references/open-vsx.md new file mode 100644 index 0000000..d993196 --- /dev/null +++ b/skills/publish-vscode-extension/references/open-vsx.md @@ -0,0 +1,151 @@ +# Open VSX Registry specifics + +The Open VSX Registry (https://open-vsx.org) is the Eclipse Foundation's vendor-neutral alternative to the VS Code Marketplace. VS Codium, Gitpod, Theia, Eclipse Che, Cursor, Windsurf, code-server, and most non-Microsoft VS Code distributions pull extensions from here — Microsoft's Marketplace ToS forbids non-Microsoft products from using it. + +The CLI is `ovsx`, published to npm. Installation: `npm install -g ovsx`. It can also be invoked as `npx ovsx`. + +## One-time account setup + +Two accounts must be linked before you can publish, because Open VSX uses GitHub for login but the publisher contract is legally with the Eclipse Foundation. + +1. **Create an Eclipse Foundation account** at https://accounts.eclipse.org/user/register. The **GitHub Username** field must exactly match the GitHub account you'll log into open-vsx.org with — Open VSX links accounts by this string. +2. **Log into https://open-vsx.org** using "Log In with GitHub." +3. Go to **Profile Settings** and complete the **Log in with Eclipse** step so the two accounts are linked. +4. Accept the **Publisher Agreement**. This is **not the same as the Eclipse Contributor Agreement (ECA)** — it's a separate document specific to Open VSX, and a publish call returns a clear error if it isn't signed yet. The signing page lives under the Eclipse profile, not under open-vsx.org. + +If the user has previously contributed to Eclipse projects and signed the ECA, they still must sign the Publisher Agreement separately. + +## Generate an access token + +1. Open-vsx.org → avatar → **Settings** → **Access Tokens** → **Generate New Token**. +2. Give it a description (e.g. `local`, `ci-github-actions`). +3. **Copy the token immediately** — it isn't shown again. Store it as `OVSX_PAT` in the shell that launches the agent, or as a CI secret. + +Unlike Marketplace PATs, Open VSX tokens don't expire. Rotate manually if you suspect leakage. + +See SKILL.md § "Handling access tokens" for the safe-handling rules — the token must never be pasted into the conversation, and the agent must reference it by env-var name (`OVSX_PAT`), never substitute the value on the command line. + +## Namespaces + +A namespace on Open VSX is what `publisher` on Marketplace is — the prefix in `publisher.extension`. Valid namespace names match `[\w\-\+\$~]+` (letters, digits, `_`, `-`, `+`, `$`, `~`). + +Important: registering a Marketplace publisher does **not** register the matching Open VSX namespace, and they live in entirely separate trust domains. Anyone can call `ovsx create-namespace foo` and immediately publish under `foo.*` — to prevent typosquatting your brand, claim the namespace early. + +```bash +ovsx create-namespace +``` + +With `OVSX_PAT` exported in the environment, `ovsx` picks it up automatically — no `-p` flag needed. This creates the namespace and adds the calling user as a **contributor**. Contributors can publish but cannot manage members. The namespace has no **owner** yet — see "verified vs unverified" below. + +## Verified vs unverified extensions + +When users browse Open VSX, extensions show one of two states: + +- **Verified** ✅ — namespace has at least one owner AND the publishing user is a namespace member. This is the steady state you want. +- **Unverified** ⚠️ — namespace has no owner yet, or the publisher isn't a member. Users see a yellow warning on the extension page. Functional, but it looks bad. + +To move from unverified to verified, the namespace needs an owner — and only the Open VSX admins can assign the first one. + +### Claim ownership of a namespace + +1. Log into open-vsx.org so admins know which Eclipse user is making the claim. +2. File a public issue at https://github.com/EclipseFdn/open-vsx.org/issues/new/choose, using the "Publisher Agreement / namespace ownership" template. +3. Include: the namespace name, evidence you own the corresponding brand (link to the GitHub org, the Marketplace publisher page, a project README, etc.), and the Eclipse account username. +4. Wait for an admin to grant ownership. The issue is public so others can object before it's processed — this is the typosquatting check. + +Once you're the owner, the extension's "unverified" badge clears on the next publish. + +### Add additional members + +Owners get a **Namespaces** section in profile settings. From there: + +- **Owner** — full authority, including adding/removing members. Multiple humans on a team should each be owners. +- **Contributor** — can publish but cannot manage members. Use this role for **service accounts and CI tokens** so a leaked CI token can't be used to reassign ownership. + +History note (December 17, 2020): namespaces used to be public — anyone could publish to a namespace they hadn't claimed. That's gone. Only members can publish now. Orphaned namespaces from that era had their previous publishers automatically added as contributors. + +### The `@open-vsx` exception + +A privileged service account `@open-vsx` can publish to any namespace, even without membership. It's used by the community-managed [`open-vsx/publish-extensions`](https://github.com/open-vsx/publish-extensions) repo, which mirrors high-demand extensions whose authors haven't published to Open VSX themselves. If a user complains "someone else's account already published my extension to Open VSX", that's almost always `@open-vsx` mirroring — opening a PR to that repo lets them take over publishing themselves. + +## Publishing commands + +All examples assume `OVSX_PAT` is exported in the environment (see SKILL.md § "Handling access tokens"). `ovsx` picks it up automatically — do **not** add `-p $OVSX_PAT` on the command line; the env-var path keeps the value out of process listings and shell history. + +```bash +# publish a pre-built .vsix (recommended — produced by vsce package) +ovsx publish ./my-ext-1.0.0.vsix + +# package-and-publish from source +ovsx publish +ovsx publish --yarn # use yarn instead of npm for the build + +# publish a pre-release channel build +ovsx publish ./my-ext.vsix --pre-release + +# publish a platform-specific build (same target names as vsce) +ovsx publish --target linux-x64 ./linux-x64.vsix +``` + +The CLI also has: + +```bash +# manage stored tokens (interactive — user runs these, not the agent) +ovsx login +ovsx logout + +# inspect what's already published +ovsx get . # download latest .vsix +ovsx get . -o ./out.vsix # to a specific path +ovsx get . --metadata # JSON metadata, no binary + +# verify a PAT is valid +ovsx verify-pat +``` + +Useful flags: + +- `-p, --pat ` — override the env-var token. Only use this with another env-var name (`-p "$MY_OTHER_PAT"`) — never substitute a literal value. +- `-r, --registryUrl ` — publish to a different Open VSX instance (e.g. an internal mirror at a corporate Theia install). Defaults to `https://open-vsx.org`. +- `OVSX_STORE=file` — disable the system keychain (useful in Docker / headless CI containers where `libsecret` isn't installed). + +## Post-publish processing + +After `ovsx publish` returns, the extension appears with status **Deactivated** while async scanning runs. This usually completes in 5–10 seconds, longer for large extensions. Scans include: + +- **Secret detection** — flags leaked AWS keys, GitHub tokens, etc. To suppress a false positive on a specific line, add a `// secret-detector:ignore` comment. Don't blanket-suppress; admins do review escalations. +- **Blocklist** — file hashes are compared against known-malicious artifacts. +- **Namespace similarity** — typosquatting check against existing high-traffic namespaces. + +If a publish gets flagged, it stays Deactivated and you'll get notified via the linked Eclipse email. Fix the issue and republish a new patch version — you can't reactivate the same version. + +## CI integration + +The community-standard GitHub Action is `HaaLeo/publish-vscode-extension`. It wraps `ovsx` (and `vsce`), so you don't need to install `ovsx` separately in the workflow: + +```yaml +- uses: HaaLeo/publish-vscode-extension@v1 + with: + pat: ${{ secrets.OVSX_PAT }} + extensionFile: ./my-ext-1.0.0.vsix + # registryUrl defaults to https://open-vsx.org +``` + +For a self-hosted publish step: + +```yaml +- run: npx ovsx publish ./my-ext-${{ env.VERSION }}.vsix -p ${{ secrets.OVSX_PAT }} +``` + +Add the CI service account to the namespace as a **Contributor** (not Owner) so a token leak limits blast radius. + +## Common errors + +| Symptom | Likely cause | Fix | +|---|---|---| +| `Unknown publisher` / namespace doesn't exist | `ovsx create-namespace` was never run, or was run on a different account | Run `ovsx create-namespace -p $OVSX_PAT`. | +| `Insufficient access rights` on publish | Token's user isn't a member of the namespace | Add the user as Contributor or Owner in the namespace settings, or use a different token. | +| `Publisher Agreement not signed` | Eclipse profile is missing the Open-VSX-specific agreement (separate from ECA) | Sign it under the Eclipse account page, then re-publish. | +| Extension shows ⚠️ "unverified" after publish | Namespace has no owner | File the namespace-ownership claim issue (see above). | +| `Mismatching publisher` | `publisher` field in `package.json` doesn't equal the namespace being published to | Make them match — Open VSX won't override the manifest. | +| Publish fails on a clean `.vsix` that worked yesterday | Token expired or was revoked, or scanner blocklisted the artifact | `ovsx verify-pat `; check the Open VSX email notification. | diff --git a/skills/publish-vscode-extension/references/vscode-marketplace.md b/skills/publish-vscode-extension/references/vscode-marketplace.md new file mode 100644 index 0000000..7bc9b04 --- /dev/null +++ b/skills/publish-vscode-extension/references/vscode-marketplace.md @@ -0,0 +1,119 @@ +# VS Code Marketplace specifics + +The VS Code Marketplace (`marketplace.visualstudio.com`) is operated by Microsoft as part of Azure DevOps. Identity, billing, and authentication all flow through a Microsoft account and an Azure DevOps organization, which is the part most users haven't seen before. + +## Identity model + +Two things you have to register before the first publish: + +1. **Azure DevOps account + organization.** This is what owns the Personal Access Token used by `vsce`. Anyone with a Microsoft account can create one for free at https://dev.azure.com. +2. **A Marketplace publisher.** This is the `publisher` field in `package.json`. Created separately at https://marketplace.visualstudio.com/manage. The publisher ID becomes part of the marketplace URL (`marketplace.visualstudio.com/items?itemName=.`) and **cannot be changed** once chosen. + +Both must use the **same Microsoft account**, or `vsce login` will succeed against Azure DevOps but every publish will return 401/403 — a confusing failure mode worth checking first when authentication looks broken. + +## Create the Personal Access Token (PAT) + +1. Sign in at https://dev.azure.com with the Microsoft account that will own the publisher. +2. Click your avatar → **User settings** → **Personal access tokens**. +3. **New Token** with: + - **Organization**: `All accessible organizations` (not just the current one — Marketplace lives in a separate Microsoft-owned org, so a single-org PAT will 401). + - **Expiration**: up to 1 year. Set a calendar reminder; expired PATs are the most common publish failure. + - **Scopes**: `Custom defined` → **Show all scopes** → **Marketplace** → tick **Manage**. (Default scopes do not include Marketplace at all.) +4. Copy the token immediately — it's shown only once. + +## Create the publisher + +1. Visit https://marketplace.visualstudio.com/manage, signed into the same Microsoft account. +2. **Create publisher**: + - **ID**: permanent. Lowercase letters/numbers/hyphens. This is what goes in `package.json`'s `publisher` field. Choose carefully — see the note on Open VSX namespace parity in `open-vsx.md`. + - **Name**: display name shown on the extension page. Can be changed later. +3. Save. + +## Authenticate `vsce` + +See SKILL.md § "Handling access tokens" first — the token must never enter the chat. + +For an agent-driven publish, set `VSCE_PAT` in the shell that launches the agent; `vsce publish` reads it automatically, no login step required. + +For interactive local use, the user can run `vsce login ` and paste the PAT when the CLI prompts for it. That value lands in the OS keychain (macOS Keychain, Windows Credential Manager, libsecret on Linux) and subsequent `vsce publish` calls reuse it. The agent should not invoke `vsce login` itself — there's nothing useful it can do with the prompt. + +## Publishing commands + +```bash +# package-and-publish in one step (also bumps the version) +vsce publish patch +vsce publish minor +vsce publish major +vsce publish 1.5.3 + +# publish a pre-built .vsix +vsce publish --packagePath ./my-ext-1.0.0.vsix + +# publish a pre-release channel build +vsce publish --pre-release + +# publish a platform-specific build +vsce publish --target win32-x64 --packagePath ./win32-x64.vsix +``` + +When invoked inside a git repository on a clean tree, `vsce publish ` also creates a version-bump commit and tag. Suppress with `--no-git-tag-version`; override the commit message with `-m "Release v%s"`. + +## Verified publisher badge + +The blue checkmark next to a publisher name. Eligibility: + +- Publisher must have an extension on the marketplace for at least **6 months**. +- The domain you're verifying must be registered for at least **6 months**. +- You must control DNS for the domain. + +Process: + +1. Publisher details page → **Verified domain** → enter the domain. +2. Marketplace shows a TXT record value. +3. Add it under the apex domain in your DNS provider. Subdomains are not eligible. +4. Click **Verify**. +5. Microsoft reviews within ~5 business days. + +The domain must serve HTTPS and return HTTP 200 on `HEAD /`. + +## Unpublish vs. remove + +These are not the same operation, and only one is recoverable. + +- **Unpublish** (recoverable): hides the extension from search and installs, but preserves stats, reviews, and the name. Done from the management page: **More Actions** → **Unpublish**. +- **Remove** (irreversible): wipes the extension and **permanently reserves the name** — nobody, including you, can ever publish another extension under that `publisher.extension` pair. Done via `vsce unpublish .` or **More Actions → Remove**. + +Use unpublish for "this was a mistake, I'll re-release shortly." Use remove only when the extension shouldn't exist (compromise, mistaken trademark, etc.). + +## Deprecation + +Marketplace doesn't expose deprecation via `vsce` — request it by filing a discussion at https://github.com/microsoft/vscode-discussions/discussions/1. You can deprecate with: + +- no replacement, +- a pointer to an alternative extension, or +- a pointer to a built-in setting that subsumes the functionality. + +The marketplace renders the extension's name struck-through with a yellow warning icon, and (if alternatives are configured) offers users a **Migrate** button. + +## Common errors + +| Symptom | Likely cause | Fix | +|---|---|---| +| `401 Unauthorized` on every publish | PAT scope is `User Profile` only, not `Marketplace (Manage)` | Reissue PAT with the right scope; do not edit existing PAT scopes — Azure DevOps doesn't let you. | +| `403 Forbidden` on every publish | PAT organization is "current" not "All accessible organizations" | Reissue PAT with `All accessible organizations`. | +| `Make sure to edit your publisher` / publisher not found | The Microsoft account that owns the PAT is not the account that owns the publisher | Re-create one of them under the matching account. | +| `You exceeded the number of allowed tags of 30` | `keywords` array > 30 entries | Trim `keywords` in `package.json`. | +| `Missing publisher name` | `publisher` field absent from `package.json` | Add it. `vsce` cannot infer it. | +| `ERR_INVALID_ARG_TYPE … repository` | `repository` is a string not an object | Use `{ "type": "git", "url": "..." }` form. | +| Extension shows but icon is missing | Icon is SVG, or smaller than 128×128, or the path in `package.json` doesn't match the file shipped in the `.vsix` | Re-export as ≥128×128 PNG; verify with `vsce ls`. | +| `Extension name already taken` | Someone (possibly you, under another account) reserved this `publisher.extension` pair | Pick a new extension name; remember `remove` is permanent. | + +## Inspecting before publish + +```bash +vsce ls # list files that will be packaged +vsce show . # show current marketplace metadata +vsce package --no-yarn # force npm even when yarn.lock is present +``` + +Running `vsce ls` before the first publish catches almost every "I accidentally shipped my `.env`" disaster.