From 3496f845b9c87d5d9b6741f04e09901351f78305 Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Thu, 10 Sep 2026 01:32:01 -0400 Subject: [PATCH 1/5] Release v0.3.0 Bump the version, add the v0.3.0 changelog entry, and backfill the missing v0.2.1 entry. Fixes found while reviewing the changes since v0.2.1: - Contribute `dexter.restart` in package.json. The command was registered in code but never declared, so it did not appear in the Command Palette. The auto-update notification tells users to restart, so they need it. - Register the restart command before `client.start()`, so it still exists when startup fails, and await the restart so errors are reported. - Expose the `dexter.maxTransientDocuments` LSP option that upstream documents. Release process: - Document that releases are published by hand. The `.gitlab-ci.yml` file does not run on GitHub, and it never published to a marketplace anyway. - Add `package-openvsx` and the publish targets. The two marketplaces use different publisher namespaces (`remoteoss` on the VS Code Marketplace, `remote-com-oss` on Open VSX) and a .vsix embeds its publisher, so each one needs its own package. - Make `make release` check that the tag matches package.json. Docs: - Document `editor.defaultFormatter` for both editors. This release stopped forcing format on save off for Elixir, so users may need to set it. - Add a Commands section. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 11 +- Makefile | 37 +- README.md | 64 ++- package-lock.json | 1235 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 16 +- src/extension.ts | 24 +- 6 files changed, 1337 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5b27b..96bb92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Changelog -## Unreleased +## v0.3.0 - Automatically download a checksum-verified Dexter binary from GitHub releases when one is not already installed - Check extension-managed binaries for updates daily without modifying system installations - Prefer an explicitly configured binary, then a Dexter installation on `PATH`, before using the extension-managed binary -- Respect global and combined-language `editor.formatOnSave` settings +- Respect global and combined-language `editor.formatOnSave` settings — the extension no longer forces format on save off for Elixir +- Best-effort support for a Windows `dexter.binary`; automatic installation stays unavailable there +- Added the **Dexter: Restart Language Server** command to the Command Palette +- Added the `dexter.maxTransientDocuments` setting + +## v0.2.1 + +- Expanded the documented feature list to cover autocompletion, hover documentation, rename, signature help, workspace symbols, code actions, and call hierarchy ## v0.2.0 diff --git a/Makefile b/Makefile index 932ac56..6618e19 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install build watch package install-vscode install-cursor install clean +.PHONY: install build watch package package-openvsx install-vscode install-cursor install publish publish-vscode publish-openvsx release clean install: install-cursor @@ -11,10 +11,25 @@ build: install-deps watch: install-deps npm run watch +PKG_VERSION := $(shell node -p "require('./package.json').version") + +# The two marketplaces use different publisher namespaces. A .vsix embeds its +# publisher, so each marketplace needs its own package. +PUBLISHER_VSCODE := remoteoss +PUBLISHER_OPENVSX := remote-com-oss + +VSIX := dexter-lsp-$(PKG_VERSION).vsix +VSIX_OPENVSX := dexter-lsp-$(PKG_VERSION)-openvsx.vsix + package: build - npx @vscode/vsce package + npx @vscode/vsce package --out $(VSIX) -VSIX := dexter-lsp-$(shell node -p "require('./package.json').version").vsix +# Repackages with the Open VSX publisher, then always restores package.json. +package-openvsx: build + @set -e; \ + trap 'npm pkg set publisher=$(PUBLISHER_VSCODE)' EXIT; \ + npm pkg set publisher=$(PUBLISHER_OPENVSX); \ + npx @vscode/vsce package --out $(VSIX_OPENVSX) install-vscode: package code --install-extension $(VSIX) @@ -22,8 +37,22 @@ install-vscode: package install-cursor: package cursor --install-extension $(VSIX) +# Needs VSCE_PAT. Publishes to the VS Code Marketplace as $(PUBLISHER_VSCODE). +publish-vscode: package + npx @vscode/vsce publish --packagePath $(VSIX) + +# Needs OVSX_PAT. Publishes to Open VSX (Cursor) as $(PUBLISHER_OPENVSX). +publish-openvsx: package-openvsx + npx ovsx publish $(VSIX_OPENVSX) + +publish: publish-vscode publish-openvsx + release: - @if [ -z "$(VERSION)" ]; then echo "Usage: make release VERSION=0.2.0"; exit 1; fi + @if [ -z "$(VERSION)" ]; then echo "Usage: make release VERSION=0.3.0"; exit 1; fi + @if [ "$(VERSION)" != "$(PKG_VERSION)" ]; then \ + echo "VERSION=$(VERSION) does not match package.json ($(PKG_VERSION)). Bump package.json first."; \ + exit 1; \ + fi @git tag v$(VERSION) @git push origin v$(VERSION) @echo "Tagged and pushed v$(VERSION)" diff --git a/README.md b/README.md index 2553b46..b8e923c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - Autocompletion with snippet support across aliases, imports, and `use` injections - Hover documentation for `@doc`, `@moduledoc`, `@typedoc`, and `@spec` - Near-instant format on save, including support for any formatter plugins your project has configured, like Styler -- Syntax highlighting for Elixir, EEx, HEEx, and LiveBook +- Syntax highlighting for Elixir, EEx, HEEx, and Livebook - Monorepo-aware with automatic reindexing on git branch switches See the [Dexter repo](https://github.com/remoteoss/dexter) for the full feature list. @@ -38,10 +38,14 @@ Dexter stores its project index in `.dexter/`. The directory manages its own `.g | `dexter.autoInstall` | `true` | Download Dexter into extension storage when it is not installed | | `dexter.autoUpdate` | `true` | Keep the extension-managed Dexter binary up to date | | `dexter.followDelegates` | `true` | Follow `defdelegate` to the target function definition | +| `dexter.maxTransientDocuments` | `50` | Buffers the server keeps for files the editor never opened. `0` disables caching | | `dexter.stdlibPath` | `""` | Path to the Elixir stdlib `lib/` directory. Auto-detected if not set | | `dexter.debug` | `false` | Enable verbose LSP logging (view with **Output → Dexter**) | -To enable format on save globally: +### Format on save + +The extension does not change your format-on-save settings. To turn format on save on +globally: ```json { @@ -49,7 +53,7 @@ To enable format on save globally: } ``` -Or enable it only for Dexter-supported languages: +Or turn it on only for the languages Dexter supports: ```json { @@ -57,6 +61,27 @@ Or enable it only for Dexter-supported languages: } ``` +If another Elixir extension is installed, set Dexter as the default formatter. The +extension ID is different in each editor: + +```json +// VS Code +{ + "[elixir]": { "editor.defaultFormatter": "remoteoss.dexter-lsp" } +} + +// Cursor +{ + "[elixir]": { "editor.defaultFormatter": "remote-com-oss.dexter-lsp" } +} +``` + +## Commands + +| Command | Description | +|---------|-------------| +| **Dexter: Restart Language Server** | Stops and starts the language server. Use it after an update, or if the server stops responding | + ## Development ```sh @@ -67,11 +92,34 @@ make install # installs to Cursor by default, or: make install-vscode ### Releasing -1. Create a release branch, bump the version in `package.json`, and update `CHANGELOG.md` -2. Merge the branch into `main` -3. Tag and push: +Releases are published by hand. There is no CI pipeline for this repository yet — the +`.gitlab-ci.yml` file is left over from GitLab and does not run on GitHub. + +The two marketplaces use different publisher namespaces, so each one needs its own +package: + +| Editor | Marketplace | Extension ID | +|--------|-------------|--------------| +| VS Code | Visual Studio Marketplace | `remoteoss.dexter-lsp` | +| Cursor | Open VSX | `remote-com-oss.dexter-lsp` | + +`make package-openvsx` sets the Open VSX publisher, builds the package, then restores +`package.json`. + +1. Create a release branch. Bump the version in `package.json` and add a `CHANGELOG.md` + entry. +2. Merge the branch into `main`, then check out `main`. +3. Publish to both marketplaces: + ```sh + export VSCE_PAT=... # Visual Studio Marketplace token + export OVSX_PAT=... # Open VSX token + make publish + ``` + Or publish to one at a time with `make publish-vscode` and `make publish-openvsx`. +4. Tag and push. The tag must match the version in `package.json`: ```sh - make release VERSION=0.2.0 + make release VERSION=0.3.0 ``` +5. Create a GitHub release for the tag and attach `dexter-lsp-.vsix`. -CI will pick up the tag and publish the extension automatically. +To build the packages without publishing, run `make package` and `make package-openvsx`. diff --git a/package-lock.json b/package-lock.json index f3fb0d6..ca3a5a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dexter-lsp", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dexter-lsp", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "vscode-languageclient": "^9.0.1" @@ -15,6 +15,7 @@ "@types/node": "^20.0.0", "@types/vscode": "^1.75.0", "@vscode/vsce": "^3.7.1", + "ovsx": "^1.1.1", "typescript": "^5.0.0" }, "engines": { @@ -185,45 +186,932 @@ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.1.tgz", "integrity": "sha512-71grXU6+5hl+3CL3joOxlj/AW6rmhthuTlG0fRqsTrhPArQBpZuUFzCIlKOGdcafLUa/i1hBdV78ZxJdlvRA+g==", "dev": true, - "dependencies": { - "@azure/msal-common": "16.4.0", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, + "dependencies": { + "@azure/msal-common": "16.4.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32/-/crc32-1.10.7.tgz", + "integrity": "sha512-OwuyRAe9Lj0GoFVBFzTS6bTAV4i+Xd68sYbNUmLnI1GVkQIqVb9mzNrpKSZkBFiQD73CdMB/uKYsvk2bxq/eZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/crc32-android-arm-eabi": "1.10.7", + "@node-rs/crc32-android-arm64": "1.10.7", + "@node-rs/crc32-darwin-arm64": "1.10.7", + "@node-rs/crc32-darwin-x64": "1.10.7", + "@node-rs/crc32-freebsd-x64": "1.10.7", + "@node-rs/crc32-linux-arm-gnueabihf": "1.10.7", + "@node-rs/crc32-linux-arm64-gnu": "1.10.7", + "@node-rs/crc32-linux-arm64-musl": "1.10.7", + "@node-rs/crc32-linux-x64-gnu": "1.10.7", + "@node-rs/crc32-linux-x64-musl": "1.10.7", + "@node-rs/crc32-win32-arm64-msvc": "1.10.7", + "@node-rs/crc32-win32-ia32-msvc": "1.10.7", + "@node-rs/crc32-win32-x64-msvc": "1.10.7" + } + }, + "node_modules/@node-rs/crc32-android-arm-eabi": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm-eabi/-/crc32-android-arm-eabi-1.10.7.tgz", + "integrity": "sha512-mWNghDkwgoc5uJhhPx/LbgLoNAxnq6Sfz4pTxi4NWG7s5l+dPe7K+L4kXb0oOfwl3Yq3Ro7CNPGLrU4cmTXhaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-android-arm64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm64/-/crc32-android-arm64-1.10.7.tgz", + "integrity": "sha512-lLUVm2H5HrSm/g2379JBRsSr5RCDqobP6I5sxZd+wSq6WBH2b6goilNYhxbss8gYQZLCJA09EvyFkZFFU9VuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-darwin-arm64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-arm64/-/crc32-darwin-arm64-1.10.7.tgz", + "integrity": "sha512-8slhmrRa3B3/z+MnLgST+lR5T0Oic60YTQ3S9OIHP259GiktlmdYyebhAvy0bBcyCuScLDcW0EB3sXxWw1TCbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-darwin-x64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-x64/-/crc32-darwin-x64-1.10.7.tgz", + "integrity": "sha512-+RkQ2+jSWco0WqSPq7GWJlJwmJpIGA79OoToGTSAsqgypkls4i3PlexVTPrfepuHn3whpa4GrmjvLbRm5RXgCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-freebsd-x64": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-freebsd-x64/-/crc32-freebsd-x64-1.10.7.tgz", + "integrity": "sha512-yqtsf23JCO1gYt3/Rkk5aoVn1rINk/J7//A9EKwGstNpBBlN3t0JKD3QpGzEnqc1vS72lEvIeO9sO3XiH+hJWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm-gnueabihf": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm-gnueabihf/-/crc32-linux-arm-gnueabihf-1.10.7.tgz", + "integrity": "sha512-AB/I1GO9LDIIHiur2bexqPy30740AmBjEhO8ij9k9desVO41dN5/ddTU2+xbf9TYt2XXvku1qYbtf5YHAAVFAw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm64-gnu": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-gnu/-/crc32-linux-arm64-gnu-1.10.7.tgz", + "integrity": "sha512-0lPFuudkW+bpeXUZAqz6FSml6Z2JvF52UGSALvnDS8Qyg3wKBNWjcXy78Pw1wiIGao/AAFFr74I3OwCMzYujuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm64-musl": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-musl/-/crc32-linux-arm64-musl-1.10.7.tgz", + "integrity": "sha512-lx5m7iCmdXPGigG7AI5NiawWsiJ1HwR/w6PV8XN48HNsgzdjar0ISB8TSLtJpq+jFcj10Wcz3Rt43RA72652Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-x64-gnu": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-gnu/-/crc32-linux-x64-gnu-1.10.7.tgz", + "integrity": "sha512-If3ogA9D2XKKb8vHttu+jl3BTWT7UCTgmrXmUAwQABH45iK32AZ9OoMcrg2rWKKXgGpG6x3IslFBgzdOkObDnA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-x64-musl": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-musl/-/crc32-linux-x64-musl-1.10.7.tgz", + "integrity": "sha512-XxuxZYJhQBlo2rRnyOOummZSyBdC+jHpXWM9XSWs1LWfsgc36IauKY+XXRAJwKwefx8lwmMjqp6T/Zq9frU87w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20" + "node": ">= 10" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "node_modules/@node-rs/crc32-win32-arm64-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-arm64-msvc/-/crc32-win32-arm64-msvc-1.10.7.tgz", + "integrity": "sha512-Rs8TGuvxgpkteSLhPqAb3DOqqRO1jiYghdMxozl+P3Ftep1hTWL8+gwYF5dtu1BtdOJpz23A+8GehSuwl96MrQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">= 10" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@node-rs/crc32-win32-ia32-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-ia32-msvc/-/crc32-win32-ia32-msvc-1.10.7.tgz", + "integrity": "sha512-G/xNcmblMH6Z7KBX02XJPJj2UGo6eTgFa+r5ZU17O8V5P9iQLrKqs/T7Sz40otDxazD1maSXT/zA8Q7rBu2/3g==", + "cpu": [ + "ia32" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">= 10" } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "node_modules/@node-rs/crc32-win32-x64-msvc": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-x64-msvc/-/crc32-win32-x64-msvc-1.10.7.tgz", + "integrity": "sha512-1uXrMu17uz12WPdii84NXmoQsoPdmSQViyCMJPu0KQuwnhErQCA4PoGK+dih9jY0mrcLuzR1r5wXVaE049fJ6w==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">= 10" } }, "node_modules/@nodelib/fs.scandir": { @@ -1037,6 +1925,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -1086,6 +1981,23 @@ "dev": true, "optional": true }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -1140,6 +2052,26 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cross-keychain": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cross-keychain/-/cross-keychain-1.1.0.tgz", + "integrity": "sha512-244DWNdGepLKD5vEn3reZqwzZFiE/LD4U+XV9IaXQbtIXKvQkf0VkRaOj/9vPYauPdR12PSGB3U0cE7jJi3WTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.8.6", + "meow": "^14.0.0" + }, + "bin": { + "cross-keychain": "dist/cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@napi-rs/keyring": "^1.2.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1253,6 +2185,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -1265,6 +2215,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1554,6 +2522,27 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1732,6 +2721,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", @@ -1779,6 +2785,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1955,6 +2974,19 @@ "dev": true, "optional": true }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -2018,6 +3050,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-it-type": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/is-it-type/-/is-it-type-5.1.3.tgz", + "integrity": "sha512-AX2uU0HW+TxagTgQXOJY7+2fbFHemC7YFBwN1XqD8qQMKdtfbOC8OC3fUb4s5NU59a3662Dzwto8tWDdZYRXxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globalthis": "^1.0.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2306,6 +3351,19 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, + "node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2519,6 +3577,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2547,6 +3615,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ovsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ovsx/-/ovsx-1.1.1.tgz", + "integrity": "sha512-tklsCzvGVWKlM91Vc9U8tNnaQ+XacPJ12SWHjDaHGUJB49oMhoAULsJGeefhHebPvvckbcWbKqKIXODMZah5SA==", + "dev": true, + "license": "EPL-2.0", + "dependencies": { + "@inquirer/prompts": "^7.10.1", + "@vscode/vsce": "^3.7.1", + "commander": "^6.2.1", + "cross-keychain": "^1.1.0", + "follow-redirects": "^1.16.0", + "is-ci": "^2.0.0", + "leven": "^3.1.0", + "semver": "^7.6.0", + "tmp": "^0.2.3", + "yauzl-promise": "^4.0.0" + }, + "bin": { + "ovsx": "bin/ovsx" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/ovsx/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -3170,6 +4273,16 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-invariant": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/simple-invariant/-/simple-invariant-2.0.1.tgz", + "integrity": "sha512-1sbhsxqI+I2tqlmjbz99GXNmZtr6tKIyEgGGnJw/MKGblalqk/XoOYYFJlBzTKZCxx8kLaD3FD5s9BEEjx5Pyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -3691,6 +4804,44 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3751,6 +4902,21 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yauzl-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yauzl-promise/-/yauzl-promise-4.0.0.tgz", + "integrity": "sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@node-rs/crc32": "^1.7.0", + "is-it-type": "^5.1.2", + "simple-invariant": "^2.0.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/yazl": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", @@ -3759,6 +4925,19 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index ac6eb0a..cd06ba6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dexter-lsp", "displayName": "Dexter", "description": "A lightning fast Elixir LSP server.", - "version": "0.2.1", + "version": "0.3.0", "publisher": "remoteoss", "license": "MIT", "repository": { @@ -30,6 +30,13 @@ ], "main": "./out/extension.js", "contributes": { + "commands": [ + { + "command": "dexter.restart", + "title": "Restart Language Server", + "category": "Dexter" + } + ], "languages": [ { "id": "elixir", @@ -153,6 +160,12 @@ "default": false, "description": "Enable debug logging to stderr." }, + "dexter.maxTransientDocuments": { + "type": "integer", + "default": 50, + "minimum": 0, + "description": "Cap on lazily-loaded buffers the server keeps in memory for files the editor never opened. Set to 0 to disable transient caching." + }, "dexter.stdlibPath": { "type": "string", "default": "", @@ -173,6 +186,7 @@ "@types/node": "^20.0.0", "@types/vscode": "^1.75.0", "@vscode/vsce": "^3.7.1", + "ovsx": "^1.1.1", "typescript": "^5.0.0" } } diff --git a/src/extension.ts b/src/extension.ts index a443760..e18375a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ export async function activate(context: vscode.ExtensionContext) { const followDelegates = config.get('followDelegates', true); const debug = config.get('debug', false); const stdlibPath = config.get('stdlibPath', ''); + const maxTransientDocuments = config.get('maxTransientDocuments', 50); const output = vscode.window.createOutputChannel('Dexter'); context.subscriptions.push(output); @@ -52,11 +53,27 @@ export async function activate(context: vscode.ExtensionContext) { initializationOptions: { followDelegates, debug, + maxTransientDocuments, ...(stdlibPath ? { stdlibPath } : {}), }, }; client = new LanguageClient('dexter', 'Dexter', serverOptions, clientOptions); + + context.subscriptions.push( + vscode.commands.registerCommand('dexter.restart', async () => { + try { + await client?.stop(); + await client?.start(); + } catch (error) { + output.appendLine(error instanceof Error ? error.stack ?? error.message : String(error)); + vscode.window.showErrorMessage( + `Dexter could not restart: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }) + ); + try { await client.start(); } catch (error) { @@ -65,13 +82,6 @@ export async function activate(context: vscode.ExtensionContext) { return; } - context.subscriptions.push( - vscode.commands.registerCommand('dexter.restart', async () => { - await client?.stop(); - client?.start(); - }) - ); - void updateManagedDexter(context, resolvedBinary, autoUpdate, output); } From f8e057b5b13a9a1ed384d378f3c0cca655f9141b Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Thu, 10 Sep 2026 14:10:27 -0400 Subject: [PATCH 2/5] Replace the GitLab pipeline with GitHub Actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `.gitlab-ci.yml` file could not run on GitHub, and it never published to a marketplace — it only uploaded a .vsix to the GitLab package registry. Two workflows replace it: - `ci.yml` compiles on pull requests and pushes to main, then packages the extension so a malformed package.json fails before a tag, not after. - `release.yml` runs on a `v*.*.*` tag. It checks the tag against package.json, builds a package for each marketplace publisher, and creates the GitHub release with that version's CHANGELOG.md section as the notes. Marketplace publishing stays manual via `make publish`. Also stop shipping dev files inside the extension. `Makefile`, `.tool-versions`, and the old `.gitlab-ci.yml` were all bundled into the .vsix. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 25 ++++++++++++ .github/workflows/release.yml | 57 +++++++++++++++++++++++++++ .gitlab-ci.yml | 72 ----------------------------------- .vscodeignore | 3 ++ README.md | 29 +++++++------- 5 files changed, 99 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .gitlab-ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..420a502 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version-file: .tool-versions + cache: npm + + - run: npm ci + + - run: npm run compile + + # Catches a malformed package.json or a missing contributed file before a tag. + - name: Verify the extension packages + run: npx @vscode/vsce package --out dexter-lsp-ci.vsix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e6fef7d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: ['v[0-9]+.[0-9]+.[0-9]+'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version-file: .tool-versions + cache: npm + + - run: npm ci + + # A tag that disagrees with package.json would publish a wrong version. + - name: Check the tag matches package.json + run: | + tag="${GITHUB_REF_NAME#v}" + pkg="$(node -p "require('./package.json').version")" + if [ "$tag" != "$pkg" ]; then + echo "Tag $GITHUB_REF_NAME does not match package.json ($pkg)." >&2 + exit 1 + fi + + # The marketplaces use different publisher namespaces and a .vsix embeds + # its publisher, so each one needs its own package. + - name: Package for both marketplaces + run: | + make package + make package-openvsx + + - name: Extract the changelog section for this version + run: | + awk -v v="${GITHUB_REF_NAME#v}" ' + $0 ~ "^## v" v "$" { found = 1; next } + found && /^## / { exit } + found { print } + ' CHANGELOG.md > release-notes.md + if [ ! -s release-notes.md ]; then + echo "No CHANGELOG.md section for $GITHUB_REF_NAME." >&2 + exit 1 + fi + cat release-notes.md + + - uses: softprops/action-gh-release@v3 + with: + body_path: release-notes.md + files: | + dexter-lsp-*.vsix diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 8ab018e..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,72 +0,0 @@ -stages: - - build - - release - -build: - stage: build - image: node:24.14.1 - cache: - key: - files: - - package-lock.json - paths: - - .npm/ - script: - - npm ci --cache .npm --prefer-offline - - npm run compile - tags: - - size-m - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - - if: '$CI_COMMIT_BRANCH == "main"' - -package: - stage: build - image: node:24.14.1 - cache: - key: - files: - - package-lock.json - paths: - - .npm/ - script: - - npm ci --cache .npm --prefer-offline - - npm run compile - - npx @vscode/vsce package - - VSIX=$(ls *.vsix) - - echo "VSIX_FILE=${VSIX}" >> build.env - - | - curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \ - --upload-file "$VSIX" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/dexter-vscode/${CI_COMMIT_TAG}/${VSIX}" - artifacts: - reports: - dotenv: build.env - expire_in: 1 week - tags: - - size-m - rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' - -release: - stage: release - image: registry.gitlab.com/gitlab-org/release-cli:latest - needs: - - job: package - artifacts: true - variables: - GITLAB_TOKEN: $GITLAB_TOKEN - script: - - echo "Creating release $CI_COMMIT_TAG" - release: - tag_name: $CI_COMMIT_TAG - description: "Release $CI_COMMIT_TAG" - assets: - links: - - name: $VSIX_FILE - url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/dexter-vscode/${CI_COMMIT_TAG}/${VSIX_FILE}" - link_type: package - tags: - - size-m - rules: - - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' diff --git a/.vscodeignore b/.vscodeignore index 08c48a8..fd77dfd 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,6 +1,9 @@ .vscode/** .vscode-test/** +.github/** src/** .gitignore +.tool-versions +Makefile tsconfig.json **/*.map diff --git a/README.md b/README.md index b8e923c..b275e6b 100644 --- a/README.md +++ b/README.md @@ -92,34 +92,33 @@ make install # installs to Cursor by default, or: make install-vscode ### Releasing -Releases are published by hand. There is no CI pipeline for this repository yet — the -`.gitlab-ci.yml` file is left over from GitLab and does not run on GitHub. - -The two marketplaces use different publisher namespaces, so each one needs its own -package: +GitHub Actions builds and packages the extension, but the two marketplaces are +published by hand. The marketplaces use different publisher namespaces, so each one +needs its own package: | Editor | Marketplace | Extension ID | |--------|-------------|--------------| | VS Code | Visual Studio Marketplace | `remoteoss.dexter-lsp` | | Cursor | Open VSX | `remote-com-oss.dexter-lsp` | -`make package-openvsx` sets the Open VSX publisher, builds the package, then restores -`package.json`. - 1. Create a release branch. Bump the version in `package.json` and add a `CHANGELOG.md` - entry. + entry with a `## v` heading. 2. Merge the branch into `main`, then check out `main`. -3. Publish to both marketplaces: +3. Tag and push. The tag must match the version in `package.json`: + ```sh + make release VERSION=0.3.0 + ``` + The `Release` workflow then builds both packages and creates the GitHub release, + using that version's `CHANGELOG.md` section as the release notes. It fails if the + tag and `package.json` disagree, or if the changelog section is missing. +4. Publish to both marketplaces: ```sh export VSCE_PAT=... # Visual Studio Marketplace token export OVSX_PAT=... # Open VSX token make publish ``` Or publish to one at a time with `make publish-vscode` and `make publish-openvsx`. -4. Tag and push. The tag must match the version in `package.json`: - ```sh - make release VERSION=0.3.0 - ``` -5. Create a GitHub release for the tag and attach `dexter-lsp-.vsix`. To build the packages without publishing, run `make package` and `make package-openvsx`. +`make package-openvsx` sets the Open VSX publisher, builds the package, then restores +`package.json`. From 8275f24502f675f6707e1dd59aae649a0b3fe63a Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Thu, 10 Sep 2026 16:07:33 -0400 Subject: [PATCH 3/5] Apply settings changes without a window reload Settings were read once during activation and baked into both serverOptions and clientOptions, so the restart command re-sent the old values and a changed `dexter.binary` was ignored until the window reloaded. Move the start into `startClient`, which reads settings and re-resolves the binary on every call. The restart command now disposes the client and builds a new one, so a restart really does apply the current settings. Changing any `dexter.*` setting offers a restart. Also validate `dexter.binary` instead of passing it straight to spawn. A path that does not exist, or one that is not executable, now reports which value is wrong and how to correct it. A `~` path is expanded, a relative path resolves against the workspace folder, and a bare command name is looked up on PATH. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 ++ README.md | 8 +++-- package.json | 2 +- src/binary.ts | 40 +++++++++++++++++++++++-- src/extension.ts | 76 ++++++++++++++++++++++++++++++++---------------- 5 files changed, 98 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96bb92e..6ecc971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Respect global and combined-language `editor.formatOnSave` settings — the extension no longer forces format on save off for Elixir - Best-effort support for a Windows `dexter.binary`; automatic installation stays unavailable there - Added the **Dexter: Restart Language Server** command to the Command Palette +- Changing a Dexter setting now offers a restart, and a restart applies the new settings +- `dexter.binary` accepts a `~` path or a workspace-relative path, and gives a clear error when the binary is missing or is not executable - Added the `dexter.maxTransientDocuments` setting ## v0.2.1 diff --git a/README.md b/README.md index b275e6b..3388c6f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Dexter stores its project index in `.dexter/`. The directory manages its own `.g | Setting | Default | Description | |---------|---------|-------------| -| `dexter.binary` | `"dexter"` | Path to the dexter binary | +| `dexter.binary` | `"dexter"` | Path to the dexter binary, or a command name to find on `PATH`. Accepts `~` and workspace-relative paths | | `dexter.autoInstall` | `true` | Download Dexter into extension storage when it is not installed | | `dexter.autoUpdate` | `true` | Keep the extension-managed Dexter binary up to date | | `dexter.followDelegates` | `true` | Follow `defdelegate` to the target function definition | @@ -76,11 +76,15 @@ extension ID is different in each editor: } ``` +Dexter settings are read when the language server starts. After you change one, the +extension offers to restart the server. You can also restart it yourself from the +Command Palette. + ## Commands | Command | Description | |---------|-------------| -| **Dexter: Restart Language Server** | Stops and starts the language server. Use it after an update, or if the server stops responding | +| **Dexter: Restart Language Server** | Restarts the language server and applies the current settings. Use it after an update, or if the server stops responding | ## Development diff --git a/package.json b/package.json index cd06ba6..b9e0df9 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "dexter.binary": { "type": "string", "default": "dexter", - "description": "Path to the dexter binary. Defaults to 'dexter' (must be on PATH)." + "description": "Path to the dexter binary, or a command name to find on PATH. Accepts ~ and workspace-relative paths. Defaults to 'dexter', which looks on PATH and otherwise installs a managed binary." }, "dexter.autoInstall": { "type": "boolean", diff --git a/src/binary.ts b/src/binary.ts index f21ffff..a44bb7e 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -2,6 +2,7 @@ import { createHash } from 'crypto'; import { execFile } from 'child_process'; import { constants, promises as fs } from 'fs'; import * as https from 'https'; +import * as os from 'os'; import * as path from 'path'; import { promisify } from 'util'; import * as vscode from 'vscode'; @@ -46,8 +47,16 @@ export async function resolveDexterBinary( output: vscode.OutputChannel, ): Promise { if (configuredBinary !== 'dexter') { - output.appendLine(`Using configured Dexter binary: ${configuredBinary}`); - return { path: configuredBinary, source: 'configured' }; + const configured = await resolveConfiguredBinary(configuredBinary); + if (!configured) { + throw new BinaryInstallError( + `the configured Dexter binary was not found or is not executable: ${configuredBinary}. ` + + 'Correct the "dexter.binary" setting, or set it back to "dexter" to let the ' + + 'extension find or install one.', + ); + } + output.appendLine(`Using configured Dexter binary: ${configured}`); + return { path: configured, source: 'configured' }; } const installedBinary = await findOnPath('dexter'); @@ -246,6 +255,33 @@ function verifyChecksum(contents: Buffer, checksums: string, archiveName: string } } +// A configured value is either a path or a bare command name to look up on PATH. +// Paths get `~` expanded, and relative ones resolve against the workspace folder, +// because the extension host's working directory is not a useful anchor. +async function resolveConfiguredBinary(configured: string): Promise { + const trimmed = configured.trim(); + if (!trimmed) { + return undefined; + } + + if (!trimmed.includes('/') && !(process.platform === 'win32' && trimmed.includes('\\'))) { + return findOnPath(trimmed); + } + + let candidate = trimmed; + if (candidate === '~' || candidate.startsWith('~/')) { + candidate = path.join(os.homedir(), candidate.slice(1)); + } + if (!path.isAbsolute(candidate)) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + candidate = workspaceFolder + ? path.join(workspaceFolder.uri.fsPath, candidate) + : path.resolve(candidate); + } + + return (await isExecutableFile(candidate)) ? candidate : undefined; +} + async function findOnPath(command: string): Promise { const pathValue = process.env.PATH; if (!pathValue) { diff --git a/src/extension.ts b/src/extension.ts index e18375a..65053f4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,11 @@ import { resolveDexterBinary, showInstallError, updateManagedDexter } from './bi let client: LanguageClient | undefined; -export async function activate(context: vscode.ExtensionContext) { +// Settings are read on every start, so a restart applies changed settings. +async function startClient( + context: vscode.ExtensionContext, + output: vscode.OutputChannel, +): Promise { const config = vscode.workspace.getConfiguration('dexter'); const configuredBinary = config.get('binary', 'dexter'); const autoInstall = config.get('autoInstall', true); @@ -17,17 +21,10 @@ export async function activate(context: vscode.ExtensionContext) { const debug = config.get('debug', false); const stdlibPath = config.get('stdlibPath', ''); const maxTransientDocuments = config.get('maxTransientDocuments', 50); - const output = vscode.window.createOutputChannel('Dexter'); - context.subscriptions.push(output); let resolvedBinary; try { - resolvedBinary = await resolveDexterBinary( - context, - configuredBinary, - autoInstall, - output, - ); + resolvedBinary = await resolveDexterBinary(context, configuredBinary, autoInstall, output); } catch (error) { output.appendLine(error instanceof Error ? error.stack ?? error.message : String(error)); await showInstallError(error); @@ -59,21 +56,6 @@ export async function activate(context: vscode.ExtensionContext) { }; client = new LanguageClient('dexter', 'Dexter', serverOptions, clientOptions); - - context.subscriptions.push( - vscode.commands.registerCommand('dexter.restart', async () => { - try { - await client?.stop(); - await client?.start(); - } catch (error) { - output.appendLine(error instanceof Error ? error.stack ?? error.message : String(error)); - vscode.window.showErrorMessage( - `Dexter could not restart: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }) - ); - try { await client.start(); } catch (error) { @@ -85,6 +67,50 @@ export async function activate(context: vscode.ExtensionContext) { void updateManagedDexter(context, resolvedBinary, autoUpdate, output); } +async function stopClient(): Promise { + const previous = client; + client = undefined; + await previous?.dispose(); +} + +export async function activate(context: vscode.ExtensionContext) { + const output = vscode.window.createOutputChannel('Dexter'); + context.subscriptions.push(output); + + const restart = async (): Promise => { + try { + await stopClient(); + await startClient(context, output); + } catch (error) { + output.appendLine(error instanceof Error ? error.stack ?? error.message : String(error)); + vscode.window.showErrorMessage( + `Dexter could not restart: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }; + + context.subscriptions.push( + vscode.commands.registerCommand('dexter.restart', restart), + ); + + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (event) => { + if (!event.affectsConfiguration('dexter')) { + return; + } + const action = await vscode.window.showInformationMessage( + 'Dexter settings changed. Restart the language server to apply them.', + 'Restart now', + ); + if (action === 'Restart now') { + await restart(); + } + }), + ); + + await startClient(context, output); +} + export async function deactivate() { - await client?.stop(); + await stopClient(); } From 7639abc061f74dba528819f3b874bdf3db3dc529 Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Thu, 10 Sep 2026 17:04:12 -0400 Subject: [PATCH 4/5] Install dependencies only when the lockfile changes `make package` ran `npm install` unconditionally, so the release workflow did it again right after `npm ci`. That wasted time and let `npm install` rewrite the lockfile in CI. Make `node_modules` a real target that depends on `package-lock.json`. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6618e19..ca0ae91 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,18 @@ install: install-cursor -install-deps: +# Reinstalls only when the lockfile is newer than the tree, so `npm ci` in CI is +# not followed by a redundant `npm install`. +node_modules: package-lock.json npm install + @touch node_modules -build: install-deps +install-deps: node_modules + +build: node_modules npm run compile -watch: install-deps +watch: node_modules npm run watch PKG_VERSION := $(shell node -p "require('./package.json').version") From 6469bfee5217cd4b623ca2195ac2ea322b6c1570 Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Thu, 10 Sep 2026 17:09:05 -0400 Subject: [PATCH 5/5] Add unit tests and polish marketplace metadata The binary resolution, checksum verification, and release asset mapping added in this release all run on a user's machine and were untested. Add a suite that covers them. The tests stub the `vscode` module through a loader hook, so they run under plain Node with the built-in test runner and need no editor and no new dependencies. CI runs them on every pull request. The asset mapping tests assert that darwin-x64 and win32 stay unmapped, so adding a target upstream does not publish would fail rather than 404 at install time. Checked against mutations: dropping `~` expansion, adding a darwin-x64 target, making the checksum compare case-sensitive, and treating the default "dexter" value as a configured path are all caught. Metadata: add the Formatters category now that the extension formats, add phoenix/heex/livebook/formatter keywords, and add homepage and bugs fields. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 2 + .vscodeignore | 1 + Makefile | 5 +- README.md | 5 + package.json | 16 +++- src/binary.ts | 17 ++-- test/binary.test.js | 197 +++++++++++++++++++++++++++++++++++++++ test/stub-vscode.js | 34 +++++++ 8 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 test/binary.test.js create mode 100644 test/stub-vscode.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 420a502..ece1294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: - run: npm run compile + - run: npm test + # Catches a malformed package.json or a missing contributed file before a tag. - name: Verify the extension packages run: npx @vscode/vsce package --out dexter-lsp-ci.vsix diff --git a/.vscodeignore b/.vscodeignore index fd77dfd..30ea77c 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -2,6 +2,7 @@ .vscode-test/** .github/** src/** +test/** .gitignore .tool-versions Makefile diff --git a/Makefile b/Makefile index ca0ae91..45aa3ed 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install build watch package package-openvsx install-vscode install-cursor install publish publish-vscode publish-openvsx release clean +.PHONY: install build watch test package package-openvsx install-vscode install-cursor install publish publish-vscode publish-openvsx release clean install: install-cursor @@ -16,6 +16,9 @@ build: node_modules watch: node_modules npm run watch +test: node_modules + npm test + PKG_VERSION := $(shell node -p "require('./package.json').version") # The two marketplaces use different publisher namespaces. A .vsix embeds its diff --git a/README.md b/README.md index 3388c6f..8501095 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,13 @@ Command Palette. git clone https://github.com/remoteoss/dexter-vscode.git cd dexter-vscode make install # installs to Cursor by default, or: make install-vscode +make test # runs the unit tests ``` +The tests cover binary resolution, checksum verification, and the release asset +mapping. They stub the `vscode` module, so they run under plain Node and need no +editor. GitHub Actions runs them on every pull request. + ### Releasing GitHub Actions builds and packages the extension, but the two marketplaces are diff --git a/package.json b/package.json index b9e0df9..2b37619 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,27 @@ "type": "git", "url": "https://github.com/remoteoss/dexter-vscode.git" }, + "homepage": "https://github.com/remoteoss/dexter", + "bugs": { + "url": "https://github.com/remoteoss/dexter-vscode/issues" + }, "engines": { "vscode": "^1.75.0" }, "icon": "dexter-logo.png", "categories": [ - "Programming Languages" + "Programming Languages", + "Formatters" ], "keywords": [ "elixir", "lsp", "go-to-definition", - "syntax-highlighting" + "syntax-highlighting", + "phoenix", + "heex", + "livebook", + "formatter" ], "activationEvents": [ "onLanguage:elixir", @@ -177,7 +186,8 @@ "scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", - "watch": "tsc -watch -p ./" + "watch": "tsc -watch -p ./", + "test": "npm run compile && node --test test/*.test.js" }, "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/src/binary.ts b/src/binary.ts index a44bb7e..43a81a5 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -125,9 +125,7 @@ export async function updateManagedDexter( execFileAsync(binary.path, ['version']), fetchLatestRelease(), ]); - const installedVersion = stdout.trim().replace(/^v/, ''); - const latestVersion = release.tag_name.replace(/^v/, ''); - if (installedVersion === latestVersion) { + if (normalizeVersion(stdout) === normalizeVersion(release.tag_name)) { output.appendLine(`Extension-managed Dexter ${release.tag_name} is up to date.`); return; } @@ -148,6 +146,10 @@ export async function updateManagedDexter( } } +export function normalizeVersion(version: string): string { + return version.trim().replace(/^v/, ''); +} + async function installLatestRelease( context: vscode.ExtensionContext, managedBinary: string, @@ -228,19 +230,22 @@ async function fetchLatestRelease(): Promise { return release as Release; } -function platformRelease(): PlatformRelease | undefined { +export function platformRelease( + platform: string = process.platform, + arch: string = process.arch, +): PlatformRelease | undefined { const names: Record = { 'darwin-arm64': 'dexter_Darwin_arm64', 'linux-arm64': 'dexter_Linux_arm64', 'linux-x64': 'dexter_Linux_x86_64', }; - const archiveDirectory = names[`${process.platform}-${process.arch}`]; + const archiveDirectory = names[`${platform}-${arch}`]; return archiveDirectory ? { archiveName: `${archiveDirectory}.tar.gz`, archiveDirectory } : undefined; } -function verifyChecksum(contents: Buffer, checksums: string, archiveName: string): void { +export function verifyChecksum(contents: Buffer, checksums: string, archiveName: string): void { const expected = checksums .split(/\r?\n/) .map((line) => line.trim().split(/\s+/)) diff --git a/test/binary.test.js b/test/binary.test.js new file mode 100644 index 0000000..a243502 --- /dev/null +++ b/test/binary.test.js @@ -0,0 +1,197 @@ +'use strict'; +const { stub } = require('./stub-vscode'); +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { createHash } = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { + resolveDexterBinary, + verifyChecksum, + platformRelease, + normalizeVersion, +} = require('../out/binary.js'); + +const sha256 = (contents) => createHash('sha256').update(contents).digest('hex'); + +test('verifyChecksum', async (t) => { + const archive = Buffer.from('archive contents'); + const name = 'dexter_Darwin_arm64.tar.gz'; + // The format GoReleaser publishes: " ", two spaces. + const checksums = `${sha256(archive)} ${name}\n${sha256('other')} dexter_Linux_arm64.tar.gz\n`; + + await t.test('accepts a matching hash', () => { + assert.doesNotThrow(() => verifyChecksum(archive, checksums, name)); + }); + + await t.test('rejects altered contents', () => { + assert.throws( + () => verifyChecksum(Buffer.from('tampered'), checksums, name), + /checksum verification failed/, + ); + }); + + await t.test('rejects an archive with no published checksum', () => { + assert.throws( + () => verifyChecksum(archive, checksums, 'dexter_Linux_x86_64.tar.gz'), + /no checksum was published/, + ); + }); + + await t.test('does not match on a filename prefix', () => { + // "dexter_Linux_arm64.tar.gz" must not satisfy a request for "dexter_Linux". + assert.throws(() => verifyChecksum(archive, checksums, 'dexter_Linux'), /no checksum/); + }); + + await t.test('accepts the binary-mode "*" filename prefix', () => { + assert.doesNotThrow(() => verifyChecksum(archive, `${sha256(archive)} *${name}\n`, name)); + }); + + await t.test('accepts CRLF line endings', () => { + assert.doesNotThrow(() => verifyChecksum(archive, `${sha256(archive)} ${name}\r\n`, name)); + }); + + await t.test('ignores hex case', () => { + const upper = `${sha256(archive).toUpperCase()} ${name}\n`; + assert.doesNotThrow(() => verifyChecksum(archive, upper, name)); + }); +}); + +test('platformRelease', async (t) => { + await t.test('maps every target upstream publishes', () => { + assert.deepEqual(platformRelease('darwin', 'arm64'), { + archiveName: 'dexter_Darwin_arm64.tar.gz', + archiveDirectory: 'dexter_Darwin_arm64', + }); + assert.deepEqual(platformRelease('linux', 'arm64'), { + archiveName: 'dexter_Linux_arm64.tar.gz', + archiveDirectory: 'dexter_Linux_arm64', + }); + assert.deepEqual(platformRelease('linux', 'x64'), { + archiveName: 'dexter_Linux_x86_64.tar.gz', + archiveDirectory: 'dexter_Linux_x86_64', + }); + }); + + await t.test('has no target where upstream publishes none', () => { + // Upstream ships no Intel macOS and no Windows build, so these must stay + // undefined and produce the "not available" message rather than a 404. + assert.equal(platformRelease('darwin', 'x64'), undefined); + assert.equal(platformRelease('win32', 'x64'), undefined); + assert.equal(platformRelease('linux', 'ia32'), undefined); + }); +}); + +test('normalizeVersion', async (t) => { + await t.test('matches `dexter version` output against a release tag', () => { + // `dexter version` prints "0.7.1"; the release tag is "v0.7.1". + assert.equal(normalizeVersion('0.7.1\n'), normalizeVersion('v0.7.1')); + }); + + await t.test('does not treat different versions as equal', () => { + assert.notEqual(normalizeVersion('0.7.1'), normalizeVersion('v0.7.2')); + }); +}); + +test('resolveDexterBinary honours dexter.binary', async (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dexter-test-')); + const home = path.join(root, 'home'); + const workspace = path.join(root, 'workspace'); + fs.mkdirSync(home); + fs.mkdirSync(path.join(workspace, 'bin'), { recursive: true }); + + const executable = path.join(home, 'dexter'); + fs.writeFileSync(executable, '#!/bin/sh\necho 0.7.1\n', { mode: 0o755 }); + const workspaceExecutable = path.join(workspace, 'bin', 'dexter'); + fs.writeFileSync(workspaceExecutable, '#!/bin/sh\n', { mode: 0o755 }); + const notExecutable = path.join(root, 'not-executable'); + fs.writeFileSync(notExecutable, 'x', { mode: 0o644 }); + + const originalHome = process.env.HOME; + process.env.HOME = home; // os.homedir() reads HOME on POSIX + stub.workspace.workspaceFolders = [{ uri: { fsPath: workspace } }]; + t.after(() => { + process.env.HOME = originalHome; + stub.workspace.workspaceFolders = undefined; + fs.rmSync(root, { recursive: true, force: true }); + }); + + const context = { + globalStorageUri: { fsPath: path.join(root, 'storage') }, + globalState: { get: () => 0, update: async () => {} }, + }; + const output = { appendLine() {} }; + // autoInstall false, so a resolution failure throws instead of downloading. + const resolve = (value) => resolveDexterBinary(context, value, false, output); + + await t.test('accepts an absolute path to an executable', async () => { + assert.deepEqual(await resolve(executable), { path: executable, source: 'configured' }); + }); + + await t.test('expands a ~ path', async () => { + assert.equal((await resolve('~/dexter')).path, executable); + }); + + await t.test('resolves a relative path against the workspace folder', async () => { + assert.equal((await resolve('bin/dexter')).path, workspaceExecutable); + }); + + await t.test('rejects a path that does not exist', async () => { + await assert.rejects(() => resolve(path.join(root, 'missing')), /was not found/); + }); + + await t.test('rejects a file that is not executable', async () => { + await assert.rejects(() => resolve(notExecutable), /was not found or is not executable/); + }); + + await t.test('rejects an empty setting', async () => { + await assert.rejects(() => resolve(' '), /was not found/); + }); + + await t.test('looks a bare command name up on PATH', async () => { + const binDirectory = path.join(root, 'onpath'); + fs.mkdirSync(binDirectory); + fs.writeFileSync(path.join(binDirectory, 'my-dexter'), '#!/bin/sh\n', { mode: 0o755 }); + const originalPath = process.env.PATH; + process.env.PATH = binDirectory; + try { + assert.equal((await resolve('my-dexter')).path, path.join(binDirectory, 'my-dexter')); + } finally { + process.env.PATH = originalPath; + } + }); + + await t.test('rejects a bare command name that is not on PATH', async () => { + await assert.rejects(() => resolve('definitely-not-on-path-xyz'), /was not found/); + }); + + await t.test('treats the default value as unconfigured and searches PATH', async () => { + // The default "dexter" must fall through to PATH discovery, not be + // treated as a configured path. + const binDirectory = path.join(root, 'defaultpath'); + fs.mkdirSync(binDirectory); + fs.writeFileSync(path.join(binDirectory, 'dexter'), '#!/bin/sh\n', { mode: 0o755 }); + const originalPath = process.env.PATH; + process.env.PATH = binDirectory; + try { + assert.deepEqual(await resolve('dexter'), { + path: path.join(binDirectory, 'dexter'), + source: 'path', + }); + } finally { + process.env.PATH = originalPath; + } + }); + + await t.test('reports that installation is disabled when nothing is found', async () => { + const originalPath = process.env.PATH; + process.env.PATH = path.join(root, 'empty'); + try { + await assert.rejects(() => resolve('dexter'), /automatic installation is disabled/); + } finally { + process.env.PATH = originalPath; + } + }); +}); diff --git a/test/stub-vscode.js b/test/stub-vscode.js new file mode 100644 index 0000000..143412e --- /dev/null +++ b/test/stub-vscode.js @@ -0,0 +1,34 @@ +'use strict'; +// The extension code imports `vscode`, which only exists inside the extension +// host. Install a loader hook that serves a stub instead, so the pure logic can +// be tested with plain Node. Require this before requiring anything from out/. +const Module = require('module'); + +const stub = { + workspace: { + workspaceFolders: undefined, + getConfiguration: () => ({ get: (_key, fallback) => fallback }), + createFileSystemWatcher: () => ({ dispose() {} }), + onDidChangeConfiguration: () => ({ dispose() {} }), + }, + window: { + createOutputChannel: () => ({ appendLine() {}, dispose() {} }), + showErrorMessage: async () => undefined, + showInformationMessage: () => ({ then() {} }), + withProgress: (_options, task) => task(), + }, + commands: { + registerCommand: () => ({ dispose() {} }), + executeCommand: async () => undefined, + }, + env: { openExternal: async () => true }, + Uri: { parse: (value) => value }, + ProgressLocation: { Notification: 15 }, +}; + +const originalLoad = Module._load; +Module._load = function (request, ...rest) { + return request === 'vscode' ? stub : originalLoad.call(this, request, ...rest); +}; + +module.exports = { stub };