Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ node_modules
server/dist/
query-results*
workshops/

.vscode-test

2 changes: 1 addition & 1 deletion extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"build": "npm run clean && npm run lint && npm run bundle",
"bundle": "node esbuild.config.js",
"bundle:server": "node scripts/bundle-server.js",
"clean": "rm -rf dist server *.vsix",
"clean": "rm -rf dist server .vscode-test/* *.vsix",
"lint": "eslint src/ test/",
"lint:fix": "eslint src/ test/ --fix",
"package": "vsce package --no-dependencies --out codeql-development-mcp-server-v$(node -e 'process.stdout.write(require(`./package.json`).version)').vsix",
Expand Down
132 changes: 128 additions & 4 deletions extensions/vscode/src/codeql/cli-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { execFile } from 'child_process';
import { access } from 'fs/promises';
import { access, readdir, readFile } from 'fs/promises';
import { constants } from 'fs';
import { dirname, join } from 'path';
import { DisposableObject } from '../common/disposable';
import type { Logger } from '../common/logger';

/** Expected binary name for the CodeQL CLI on the current platform. */
const CODEQL_BINARY_NAME = process.platform === 'win32' ? 'codeql.exe' : 'codeql';

/** Known filesystem locations where the CodeQL CLI may be installed. */
const KNOWN_LOCATIONS = [
'/usr/local/bin/codeql',
Expand All @@ -18,15 +22,20 @@ const KNOWN_LOCATIONS = [
* Detection strategy (in order):
* 1. `CODEQL_PATH` environment variable
* 2. `codeql` on `$PATH` (via `which`)
* 3. Known filesystem locations
* 3. `vscode-codeql` managed distribution (via `distribution.json` hint
* or directory scan of `distribution*` folders)
* 4. Known filesystem locations
*
* Results are cached. Call `invalidateCache()` when the environment changes
* (e.g. `extensions.onDidChange` fires).
*/
export class CliResolver extends DisposableObject {
private cachedPath: string | undefined | null = null; // null = not yet resolved

constructor(private readonly logger: Logger) {
constructor(
private readonly logger: Logger,
private readonly vsCodeCodeqlStoragePath?: string,
) {
super();
}

Expand Down Expand Up @@ -58,7 +67,15 @@ export class CliResolver extends DisposableObject {
return whichPath;
}

// Strategy 3: known filesystem locations
// Strategy 3: vscode-codeql managed distribution
const distPath = await this.resolveFromVsCodeDistribution();
if (distPath) {
this.logger.info(`CodeQL CLI found via vscode-codeql distribution: ${distPath}`);
this.cachedPath = distPath;
return distPath;
}

// Strategy 4: known filesystem locations
for (const location of KNOWN_LOCATIONS) {
const validated = await this.validateBinary(location);
if (validated) {
Expand Down Expand Up @@ -89,6 +106,113 @@ export class CliResolver extends DisposableObject {
}
}

/**
* Discover the CodeQL CLI binary from the `vscode-codeql` extension's
* managed distribution directory.
*
* The `GitHub.vscode-codeql` extension downloads the CodeQL CLI into:
* `<globalStorage>/github.vscode-codeql/distribution<N>/codeql/codeql`
*
* where `<N>` is an incrementing folder index that increases each time
* the extension upgrades the CLI. A `distribution.json` file in the
* storage root contains a `folderIndex` property that identifies the
* current distribution directory. We use that as a fast-path hint and
* fall back to scanning for the highest-numbered `distribution*` folder.
*/
private async resolveFromVsCodeDistribution(): Promise<string | undefined> {
if (!this.vsCodeCodeqlStoragePath) return undefined;

const parent = dirname(this.vsCodeCodeqlStoragePath);
// VS Code stores the extension directory as either 'GitHub.vscode-codeql'
// (original publisher casing) or 'github.vscode-codeql' (lowercased by VS Code
// on some platforms/versions). Probe both to ensure discovery works on
// case-sensitive filesystems.
const candidatePaths = [
...new Set([
this.vsCodeCodeqlStoragePath,
join(parent, 'github.vscode-codeql'),
join(parent, 'GitHub.vscode-codeql'),
]),
];

for (const storagePath of candidatePaths) {
try {
// Fast path: read distribution.json for the exact folder index
const hintPath = await this.resolveFromDistributionJson(storagePath);
if (hintPath) return hintPath;
} catch {
this.logger.debug('distribution.json hint unavailable, falling back to directory scan');
}

// Fallback: scan for distribution* directories
const scanPath = await this.resolveFromDistributionScan(storagePath);
if (scanPath) return scanPath;
}
return undefined;
}

/**
* Read `distribution.json` to get the current `folderIndex` and validate
* the binary at the corresponding path.
*/
private async resolveFromDistributionJson(storagePath: string): Promise<string | undefined> {
const jsonPath = join(storagePath, 'distribution.json');
const content = await readFile(jsonPath, 'utf-8');
const data = JSON.parse(content) as { folderIndex?: number };

if (typeof data.folderIndex !== 'number') return undefined;

const binaryPath = join(
storagePath,
`distribution${data.folderIndex}`,
'codeql',
CODEQL_BINARY_NAME,
);
const validated = await this.validateBinary(binaryPath);
if (validated) {
this.logger.debug(`Resolved CLI via distribution.json (folderIndex=${data.folderIndex})`);
return binaryPath;
}
return undefined;
}

/**
* Scan for `distribution*` directories sorted by numeric suffix (highest
* first) and return the first one containing a valid `codeql` binary.
*/
private async resolveFromDistributionScan(storagePath: string): Promise<string | undefined> {
try {
const entries = await readdir(storagePath, { withFileTypes: true });

const distDirs = entries
.filter(e => e.isDirectory() && /^distribution\d*$/.test(e.name))
.map(e => ({
name: e.name,
num: parseInt(e.name.replace('distribution', '') || '0', 10),
}))
.sort((a, b) => b.num - a.num);

for (const dir of distDirs) {
const binaryPath = join(
storagePath,
dir.name,
'codeql',
CODEQL_BINARY_NAME,
);
const validated = await this.validateBinary(binaryPath);
if (validated) {
this.logger.debug(`Resolved CLI via distribution scan: ${dir.name}`);
return binaryPath;
}
}
} catch {
this.logger.debug(
`Could not scan vscode-codeql distribution directory: ${storagePath}`,
);
}
return undefined;
}

/** Attempt to find `codeql` on PATH. */
private resolveFromPath(): Promise<string | undefined> {
return new Promise((resolve) => {
Expand Down
4 changes: 2 additions & 2 deletions extensions/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export async function activate(
logger.info('CodeQL Development MCP Server extension activating...');

// --- Core components ---
const cliResolver = new CliResolver(logger);
const storagePaths = new StoragePaths(context);
const cliResolver = new CliResolver(logger, storagePaths.getCodeqlGlobalStoragePath());
const serverManager = new ServerManager(context, logger);
const packInstaller = new PackInstaller(cliResolver, serverManager, logger);
const storagePaths = new StoragePaths(context);
const envBuilder = new EnvironmentBuilder(
context,
cliResolver,
Expand Down
Loading