Skip to content

Commit 9434252

Browse files
committed
feat(lsp): auto-install language servers with cancellable status-bar task
Removes the consent dialog + install bar + problems-panel row for the PHP and Python language servers (user feedback: too many dialogs). The servers now install automatically on the first relevant file open, announced only through the status-bar task whose popup is opened for every install/upgrade (download size in the message) with a stop icon. - cancel: downloadFile/_npmInstallInFolder return promises carrying a cancel() handle (node-side keyed RPC under the hood; AbortController for downloads, retained child process for npm). Cancel cleans partial files and defers retry to the next app launch; the master prefs (python/php.codeIntelligence) stay the durable opt-out. - npm installs on the same folder are now mutually exclusive, and the in-flight registries are cleaned on every failure path so retries always work. - offline is a non-event: no task/toast, auto-retry when connectivity returns (window online) or next launch. Network errors mid-install take the same quiet path. - real failures roll back the partial install, turn the task red with a retry icon, open the popup and raise a subtle toast; metrics (lsp/pyInst|phpInst x ok/upOk/fail/cancel/waitNet) and logger error reports added. - corrupt-but-present installs self-repair (wipe + reinstall once per session) instead of staying silently broken. - server versions now live in src/config.json (lsp_server_pins), flowing through generated appConfig.js; a missing pins block alerts (stale build tripwire). npm run updateBuiltInLSPS bumps all built-in servers (runtime pins + bundled vtsls/json in src-node) to latest stable, and a monthly workflow opens a reviewed PR gated by the LSP suites. - auto-install never runs in test windows; suites acquire servers via installNow() explicitly (new guard specs).
1 parent cd2f4d7 commit 9434252

15 files changed

Lines changed: 781 additions & 695 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Update pinned LSP server versions
2+
3+
on:
4+
schedule:
5+
# monthly - 00:00 UTC on the 1st
6+
- cron: '0 0 1 * *'
7+
workflow_dispatch:
8+
9+
jobs:
10+
update-pins:
11+
runs-on: ubuntu-22.04
12+
steps:
13+
- uses: actions/checkout@v3
14+
with:
15+
ref: main
16+
- name: Update built-in LSP server versions
17+
run: |
18+
npm run updateBuiltInLSPS
19+
git status
20+
shell: bash
21+
22+
- name: Create update-lsp-pins Pull Request
23+
id: cpr
24+
uses: peter-evans/create-pull-request@v4
25+
with:
26+
commit-message: 'chore: update built-in LSP server versions'
27+
committer: GitHub <noreply@github.com>
28+
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
29+
title: '[Release BOT] Update built-in LSP server versions'
30+
branch: bot/update-lsp-pins
31+
add-paths: |
32+
src/config.json
33+
src-node/package.json
34+
src-node/package-lock.json
35+
body: |
36+
Updates every built-in language server to its latest stable release:
37+
- runtime-installed pins in `src/config.json` (`lsp_server_pins`): intelephense (npm),
38+
pyrefly and ruff (PyPI)
39+
- bundled servers in `src-node/package.json` (+ lockfile): @vtsls/language-server (TS/JS),
40+
vscode-langservers-extracted (JSON)
41+
42+
The TS/JSON/PHP/Python LSP integration suites exercise these exact versions for real,
43+
so a green CI on this PR means the new versions actually work. Merge only when green.
44+
- Auto-generated by `update-lsp-pins.yml` action
45+
- name: Check outputs
46+
if: ${{ steps.cpr.outputs.pull-request-number }}
47+
run: |
48+
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
49+
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

build/update-lsp-pins.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify it
7+
* under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
14+
* for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
18+
*
19+
*/
20+
21+
/*
22+
* Updates every built-in language server to its latest STABLE release:
23+
* - runtime-installed pins in src/config.json (config.lsp_server_pins): intelephense (npm),
24+
* pyrefly and ruff (PyPI), and
25+
* - bundled servers in src-node/package.json: @vtsls/language-server (TS/JS) and
26+
* vscode-langservers-extracted (JSON) - written as EXACT versions, with the src-node
27+
* lockfile regenerated so `npm ci` stays green.
28+
*
29+
* Prereleases are never picked up: npm's `latest` dist-tag is stable by convention and any
30+
* version carrying a prerelease suffix is skipped with a warning.
31+
*
32+
* Run by the scheduled update-lsp-pins GitHub workflow (npm run updateBuiltInLSPS), which opens
33+
* a PR with the diff - the TS/JSON/PHP/Python LSP integration suites exercise these exact
34+
* versions for real, so CI green on that PR means the new versions actually work before they
35+
* ship. Never run as part of normal builds: pins must not drift under a developer mid-work.
36+
*
37+
* Usage: node build/update-lsp-pins.js [--dry-run]
38+
*/
39+
40+
/* eslint-env node */
41+
42+
const fs = require("fs");
43+
const path = require("path");
44+
const { execFileSync } = require("child_process");
45+
46+
const CONFIG_PATH = path.join(__dirname, "..", "src", "config.json");
47+
const SRC_NODE_DIR = path.join(__dirname, "..", "src-node");
48+
const SRC_NODE_PKG_PATH = path.join(SRC_NODE_DIR, "package.json");
49+
const DRY_RUN = process.argv.includes("--dry-run");
50+
51+
// bundled servers shipped inside src-node - bumped in src-node/package.json as exact versions
52+
const BUNDLED_LSP_PACKAGES = ["@vtsls/language-server", "vscode-langservers-extracted"];
53+
54+
async function _fetchJson(url) {
55+
const response = await fetch(url);
56+
if (!response.ok) {
57+
throw new Error("HTTP " + response.status + " for " + url);
58+
}
59+
return response.json();
60+
}
61+
62+
// npm's `latest` dist-tag - the stable channel by convention (prereleases live on other tags)
63+
async function latestNpmVersion(pkg) {
64+
const meta = await _fetchJson("https://registry.npmjs.org/" + encodeURIComponent(pkg) + "/latest");
65+
return meta.version;
66+
}
67+
68+
async function latestPyPIVersion(pkg) {
69+
const meta = await _fetchJson("https://pypi.org/pypi/" + pkg + "/json");
70+
return meta.info.version;
71+
}
72+
73+
function _isPrerelease(version) {
74+
return version.indexOf("-") !== -1;
75+
}
76+
77+
async function updateRuntimePins() {
78+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
79+
const pins = config.config.lsp_server_pins;
80+
if (!pins) {
81+
throw new Error("config.lsp_server_pins missing in " + CONFIG_PATH);
82+
}
83+
const latest = {
84+
intelephense: await latestNpmVersion("intelephense"),
85+
pyrefly: await latestPyPIVersion("pyrefly"),
86+
ruff: await latestPyPIVersion("ruff")
87+
};
88+
let changed = false;
89+
for (const key of Object.keys(latest)) {
90+
if (_isPrerelease(latest[key])) {
91+
console.warn(key + ": skipping prerelease " + latest[key] + ", keeping " + pins[key]);
92+
} else if (pins[key] !== latest[key]) {
93+
console.log(key + ": " + pins[key] + " -> " + latest[key]);
94+
pins[key] = latest[key];
95+
changed = true;
96+
} else {
97+
console.log(key + ": " + pins[key] + " (up to date)");
98+
}
99+
}
100+
if (!changed || DRY_RUN) {
101+
if (changed) {
102+
console.log("--dry-run: not writing " + CONFIG_PATH);
103+
}
104+
return;
105+
}
106+
// no trailing newline - matches the existing file and the gulp config writer exactly
107+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 4), "utf8");
108+
console.log("Updated " + CONFIG_PATH);
109+
}
110+
111+
async function updateBundledServers() {
112+
const pkgJson = JSON.parse(fs.readFileSync(SRC_NODE_PKG_PATH, "utf8"));
113+
let changed = false;
114+
for (const pkg of BUNDLED_LSP_PACKAGES) {
115+
const current = pkgJson.dependencies[pkg];
116+
if (!current) {
117+
throw new Error(pkg + " missing from src-node/package.json dependencies");
118+
}
119+
const latest = await latestNpmVersion(pkg);
120+
if (_isPrerelease(latest)) {
121+
console.warn(pkg + ": skipping prerelease " + latest + ", keeping " + current);
122+
} else if (current !== latest) {
123+
console.log(pkg + ": " + current + " -> " + latest);
124+
pkgJson.dependencies[pkg] = latest;
125+
changed = true;
126+
} else {
127+
console.log(pkg + ": " + current + " (up to date)");
128+
}
129+
}
130+
if (!changed || DRY_RUN) {
131+
if (changed) {
132+
console.log("--dry-run: not writing " + SRC_NODE_PKG_PATH);
133+
}
134+
return;
135+
}
136+
fs.writeFileSync(SRC_NODE_PKG_PATH, JSON.stringify(pkgJson, null, 4) + "\n", "utf8");
137+
console.log("Updated " + SRC_NODE_PKG_PATH);
138+
// keep the committed lockfile in sync or `npm ci` fails on manifest mismatch;
139+
// --package-lock-only avoids touching node_modules
140+
console.log("Regenerating src-node/package-lock.json ...");
141+
execFileSync("npm", ["install", "--package-lock-only"], { cwd: SRC_NODE_DIR, stdio: "inherit" });
142+
}
143+
144+
(async function main() {
145+
await updateRuntimePins();
146+
await updateBundledServers();
147+
}()).catch(function (err) {
148+
console.error(err);
149+
process.exit(1);
150+
});

docs/API-Reference/utils/NodeUtils.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,35 @@ This is only available in the native app.
7373
| [options] | <code>Object</code> | |
7474
| [options.sha256] | <code>string</code> | expected hex digest of the downloaded bytes |
7575
| [options.progress] | <code>function</code> | called with (transferredBytes, totalBytes) as the download advances; totalBytes is 0 if the server sent no length |
76+
| [options.cancelId] | <code>string</code> | registers the download so cancelDownload(cancelId) can abort it mid-stream; the partial file is deleted and the rejection error message contains "cancelled" |
77+
78+
<a name="cancelDownload"></a>
79+
80+
## cancelDownload(cancelId) ⇒ <code>Promise.&lt;void&gt;</code>
81+
Cancels an in-flight downloadFile run by the cancelId it was started with. Idempotent -
82+
unknown ids are a no-op. The cancelled download's promise rejects (message contains
83+
"cancelled") and its partial file is deleted.
84+
This is only available in the native app.
85+
86+
**Kind**: global function
87+
88+
| Param | Type | Description |
89+
| --- | --- | --- |
90+
| cancelId | <code>string</code> | cancelId passed to downloadFile |
91+
92+
<a name="_cancelNpmInstall"></a>
93+
94+
## \_cancelNpmInstall(moduleNativeDir) ⇒ <code>Promise.&lt;void&gt;</code>
95+
Kills an in-flight _npmInstallInFolder run by the moduleNativeDir it was started with.
96+
Idempotent - unknown dirs are a no-op. The killed install's promise rejects (message
97+
contains "cancelled").
98+
This is only available in the native app.
99+
100+
**Kind**: global function
101+
102+
| Param | Type | Description |
103+
| --- | --- | --- |
104+
| moduleNativeDir | <code>string</code> | dir passed to _npmInstallInFolder |
76105

77106
<a name="extractZipFile"></a>
78107

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"prepare": "husky install",
4949
"_serveTest": "http-server . -p 5000 -c-1",
5050
"zipTestFiles": "gulp zipTestFiles",
51+
"updateBuiltInLSPS": "node build/update-lsp-pins.js",
5152
"test": "echo please see `Running and debugging tests` section in readme.md",
5253
"testIntegHelp": "echo By default this command only runs unit tests.To Run integration tests, please see `Running and debugging tests` section in readme.md",
5354
"testChromium": "npm run testIntegHelp && npx playwright test --project=chromium",

0 commit comments

Comments
 (0)