Skip to content

Commit 9861e84

Browse files
data-douserCopilot
andauthored
Support automatic discovery of codeql CLI distributions installed off-PATH by VS Code extension (#91)
* auto-discover vscode-codeql managed CodeQL CLI dist The MCP server now automatically finds the CodeQL CLI binary installed by the GitHub.vscode-codeql extension, which stores it off-PATH at: <globalStorage>/github.vscode-codeql/distribution<N>/codeql/codeql Discovery uses distribution.json (folderIndex hint) with a fallback to scanning distribution* directories sorted by descending index. This is implemented at two layers: - VS Code extension CliResolver (Strategy 3, between PATH and known locations) — uses the StoragePaths-provided storage directory - Server-side cli-executor (fallback when CODEQL_PATH is unset) — probes platform-specific VS Code global storage directories for Code, Code - Insiders, and VSCodium Also fixes extension.test.ts constructor mocks for Vitest 4.x compatibility (vi.clearAllMocks instead of vi.resetAllMocks). T_EDITOR=true git rebase --continue * Implement changes for PR review comments * Fix deterministic vscode-codeql discovery tests and dual-casing path probe in CliResolver (#92) * Add getResolvedCodeQLDir() caching test assertion * auto-discover vscode-codeql managed CodeQL CLI dist The MCP server now automatically finds the CodeQL CLI binary installed by the GitHub.vscode-codeql extension, which stores it off-PATH at: <globalStorage>/github.vscode-codeql/distribution<N>/codeql/codeql Discovery uses distribution.json (folderIndex hint) with a fallback to scanning distribution* directories sorted by descending index. This is implemented at two layers: - VS Code extension CliResolver (Strategy 3, between PATH and known locations) — uses the StoragePaths-provided storage directory - Server-side cli-executor (fallback when CODEQL_PATH is unset) — probes platform-specific VS Code global storage directories for Code, Code - Insiders, and VSCodium Also fixes extension.test.ts constructor mocks for Vitest 4.x compatibility (vi.clearAllMocks instead of vi.resetAllMocks). T_EDITOR=true git rebase --continue * Implement changes for PR review comments * Fix deterministic vscode-codeql discovery tests and dual-casing path probe in CliResolver (#92) * Add getResolvedCodeQLDir() caching test assertion * Sync server/dist/** * Addres PR review comments * Address latest PR review feedback * Sync package-lock.json and server/dist/** * Address latest PR review comments * Sync package-lock.json & server/dist/ * Fix regex for CodeQL CLI dist discovery --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 1660bae commit 9861e84

File tree

9 files changed

+1014
-182
lines changed

9 files changed

+1014
-182
lines changed

extensions/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@
137137
"build": "npm run clean && npm run lint && npm run bundle",
138138
"bundle": "node esbuild.config.js",
139139
"bundle:server": "node scripts/bundle-server.js",
140-
"clean": "rm -rf dist server *.vsix",
140+
"clean": "rm -rf dist server .vscode-test/* *.vsix",
141141
"lint": "eslint src/ test/",
142142
"lint:fix": "eslint src/ test/ --fix",
143143
"package": "vsce package --no-dependencies --out codeql-development-mcp-server-v$(node -e 'process.stdout.write(require(`./package.json`).version)').vsix",

extensions/vscode/src/codeql/cli-resolver.ts

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { execFile } from 'child_process';
2-
import { access } from 'fs/promises';
2+
import { access, readdir, readFile } from 'fs/promises';
33
import { constants } from 'fs';
4+
import { dirname, join } from 'path';
45
import { DisposableObject } from '../common/disposable';
56
import type { Logger } from '../common/logger';
67

8+
/** Expected binary name for the CodeQL CLI on the current platform. */
9+
const CODEQL_BINARY_NAME = process.platform === 'win32' ? 'codeql.exe' : 'codeql';
10+
711
/** Known filesystem locations where the CodeQL CLI may be installed. */
812
const KNOWN_LOCATIONS = [
913
'/usr/local/bin/codeql',
@@ -18,15 +22,20 @@ const KNOWN_LOCATIONS = [
1822
* Detection strategy (in order):
1923
* 1. `CODEQL_PATH` environment variable
2024
* 2. `codeql` on `$PATH` (via `which`)
21-
* 3. Known filesystem locations
25+
* 3. `vscode-codeql` managed distribution (via `distribution.json` hint
26+
* or directory scan of `distribution*` folders)
27+
* 4. Known filesystem locations
2228
*
2329
* Results are cached. Call `invalidateCache()` when the environment changes
2430
* (e.g. `extensions.onDidChange` fires).
2531
*/
2632
export class CliResolver extends DisposableObject {
2733
private cachedPath: string | undefined | null = null; // null = not yet resolved
2834

29-
constructor(private readonly logger: Logger) {
35+
constructor(
36+
private readonly logger: Logger,
37+
private readonly vsCodeCodeqlStoragePath?: string,
38+
) {
3039
super();
3140
}
3241

@@ -58,7 +67,15 @@ export class CliResolver extends DisposableObject {
5867
return whichPath;
5968
}
6069

61-
// Strategy 3: known filesystem locations
70+
// Strategy 3: vscode-codeql managed distribution
71+
const distPath = await this.resolveFromVsCodeDistribution();
72+
if (distPath) {
73+
this.logger.info(`CodeQL CLI found via vscode-codeql distribution: ${distPath}`);
74+
this.cachedPath = distPath;
75+
return distPath;
76+
}
77+
78+
// Strategy 4: known filesystem locations
6279
for (const location of KNOWN_LOCATIONS) {
6380
const validated = await this.validateBinary(location);
6481
if (validated) {
@@ -89,6 +106,113 @@ export class CliResolver extends DisposableObject {
89106
}
90107
}
91108

109+
/**
110+
* Discover the CodeQL CLI binary from the `vscode-codeql` extension's
111+
* managed distribution directory.
112+
*
113+
* The `GitHub.vscode-codeql` extension downloads the CodeQL CLI into:
114+
* `<globalStorage>/github.vscode-codeql/distribution<N>/codeql/codeql`
115+
*
116+
* where `<N>` is an incrementing folder index that increases each time
117+
* the extension upgrades the CLI. A `distribution.json` file in the
118+
* storage root contains a `folderIndex` property that identifies the
119+
* current distribution directory. We use that as a fast-path hint and
120+
* fall back to scanning for the highest-numbered `distribution*` folder.
121+
*/
122+
private async resolveFromVsCodeDistribution(): Promise<string | undefined> {
123+
if (!this.vsCodeCodeqlStoragePath) return undefined;
124+
125+
const parent = dirname(this.vsCodeCodeqlStoragePath);
126+
// VS Code stores the extension directory as either 'GitHub.vscode-codeql'
127+
// (original publisher casing) or 'github.vscode-codeql' (lowercased by VS Code
128+
// on some platforms/versions). Probe both to ensure discovery works on
129+
// case-sensitive filesystems.
130+
const candidatePaths = [
131+
...new Set([
132+
this.vsCodeCodeqlStoragePath,
133+
join(parent, 'github.vscode-codeql'),
134+
join(parent, 'GitHub.vscode-codeql'),
135+
]),
136+
];
137+
138+
for (const storagePath of candidatePaths) {
139+
try {
140+
// Fast path: read distribution.json for the exact folder index
141+
const hintPath = await this.resolveFromDistributionJson(storagePath);
142+
if (hintPath) return hintPath;
143+
} catch {
144+
this.logger.debug('distribution.json hint unavailable, falling back to directory scan');
145+
}
146+
147+
// Fallback: scan for distribution* directories
148+
const scanPath = await this.resolveFromDistributionScan(storagePath);
149+
if (scanPath) return scanPath;
150+
}
151+
return undefined;
152+
}
153+
154+
/**
155+
* Read `distribution.json` to get the current `folderIndex` and validate
156+
* the binary at the corresponding path.
157+
*/
158+
private async resolveFromDistributionJson(storagePath: string): Promise<string | undefined> {
159+
const jsonPath = join(storagePath, 'distribution.json');
160+
const content = await readFile(jsonPath, 'utf-8');
161+
const data = JSON.parse(content) as { folderIndex?: number };
162+
163+
if (typeof data.folderIndex !== 'number') return undefined;
164+
165+
const binaryPath = join(
166+
storagePath,
167+
`distribution${data.folderIndex}`,
168+
'codeql',
169+
CODEQL_BINARY_NAME,
170+
);
171+
const validated = await this.validateBinary(binaryPath);
172+
if (validated) {
173+
this.logger.debug(`Resolved CLI via distribution.json (folderIndex=${data.folderIndex})`);
174+
return binaryPath;
175+
}
176+
return undefined;
177+
}
178+
179+
/**
180+
* Scan for `distribution*` directories sorted by numeric suffix (highest
181+
* first) and return the first one containing a valid `codeql` binary.
182+
*/
183+
private async resolveFromDistributionScan(storagePath: string): Promise<string | undefined> {
184+
try {
185+
const entries = await readdir(storagePath, { withFileTypes: true });
186+
187+
const distDirs = entries
188+
.filter(e => e.isDirectory() && /^distribution\d+$/.test(e.name))
189+
.map(e => ({
190+
name: e.name,
191+
num: parseInt(e.name.replace('distribution', ''), 10),
192+
}))
193+
.sort((a, b) => b.num - a.num);
194+
195+
for (const dir of distDirs) {
196+
const binaryPath = join(
197+
storagePath,
198+
dir.name,
199+
'codeql',
200+
CODEQL_BINARY_NAME,
201+
);
202+
const validated = await this.validateBinary(binaryPath);
203+
if (validated) {
204+
this.logger.debug(`Resolved CLI via distribution scan: ${dir.name}`);
205+
return binaryPath;
206+
}
207+
}
208+
} catch {
209+
this.logger.debug(
210+
`Could not scan vscode-codeql distribution directory: ${storagePath}`,
211+
);
212+
}
213+
return undefined;
214+
}
215+
92216
/** Attempt to find `codeql` on PATH. */
93217
private resolveFromPath(): Promise<string | undefined> {
94218
return new Promise((resolve) => {

extensions/vscode/src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export async function activate(
2424
logger.info('CodeQL Development MCP Server extension activating...');
2525

2626
// --- Core components ---
27-
const cliResolver = new CliResolver(logger);
27+
const storagePaths = new StoragePaths(context);
28+
const cliResolver = new CliResolver(logger, storagePaths.getCodeqlGlobalStoragePath());
2829
const serverManager = new ServerManager(context, logger);
2930
const packInstaller = new PackInstaller(cliResolver, serverManager, logger);
30-
const storagePaths = new StoragePaths(context);
3131
const envBuilder = new EnvironmentBuilder(
3232
context,
3333
cliResolver,

0 commit comments

Comments
 (0)