-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpack-installer.ts
More file actions
338 lines (313 loc) · 11.1 KB
/
pack-installer.ts
File metadata and controls
338 lines (313 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import { execFile } from 'child_process';
import { access } from 'fs/promises';
import { constants } from 'fs';
import { join } from 'path';
import { DisposableObject } from '../common/disposable';
import type { Logger } from '../common/logger';
import type { CliResolver } from '../codeql/cli-resolver';
import type { ServerManager } from './server-manager';
export interface PackInstallOptions {
/** Force reinstall even if lock files exist. */
force?: boolean;
/** Install only for specific languages. */
languages?: string[];
}
/**
* Installs CodeQL pack dependencies for the bundled tool query packs
* shipped inside the VSIX (or, as fallback, in the locally-installed
* `codeql-development-mcp-server` npm package).
*
* The VSIX bundles the qlpack source files (`.ql` + lock files) at
* `<extensionRoot>/server/ql/<lang>/tools/src/`, and the npm install
* mirrors them at `globalStorage/mcp-server/node_modules/.../ql/...`.
* The bundled copy is always preferred so that the packs used by
* `codeql pack install` match the server code running from the VSIX.
*
* When the detected CodeQL CLI version differs from the version the
* VSIX was built against, the installer downloads pre-compiled packs
* from GHCR for the matching CLI version via `codeql pack download`.
* This ensures backwards compatibility across published CLI versions.
*
* CodeQL library dependencies (e.g. `codeql/javascript-all`) must be
* fetched from GHCR via `codeql pack install`. This class automates
* the `codeql-development-mcp-server-setup-packs` step documented in
* the getting-started guide.
*/
export class PackInstaller extends DisposableObject {
static readonly SUPPORTED_LANGUAGES = [
'actions',
'cpp',
'csharp',
'go',
'java',
'javascript',
'python',
'ruby',
'swift',
] as const;
/**
* Maps CodeQL CLI versions to the ql-mcp tools pack version published
* for that CLI release. Each ql-mcp stable release is built against a
* specific CodeQL CLI version, and the published pack version matches
* the ql-mcp release version.
*
* Compatibility range: v2.24.0 (initial public release) through the
* current release.
*/
static readonly CLI_VERSION_TO_PACK_VERSION: ReadonlyMap<string, string> =
new Map([
['2.24.0', '2.24.0'],
['2.24.1', '2.24.1'],
['2.24.2', '2.24.2'],
['2.24.3', '2.24.3'],
['2.25.0', '2.25.0'],
['2.25.1', '2.25.1'],
]);
/** Pack scope/prefix for all ql-mcp tools packs on GHCR. */
static readonly PACK_SCOPE = 'advanced-security';
constructor(
private readonly cliResolver: CliResolver,
private readonly serverManager: ServerManager,
private readonly logger: Logger,
) {
super();
}
/**
* Get the root directory for qlpack resolution.
*
* Prefers the bundled `server/` directory inside the VSIX so that the
* packs installed match the server version. Falls back to the
* npm-installed package root in `globalStorage` (for local dev or when
* the VSIX bundle is missing).
*/
private getQlpackRoot(): string {
return this.serverManager.getBundledQlRoot()
?? this.serverManager.getPackageRoot();
}
/**
* Get the qlpack source directories for all languages.
*/
getQlpackPaths(): string[] {
const root = this.getQlpackRoot();
return PackInstaller.SUPPORTED_LANGUAGES.map((lang) =>
join(root, 'ql', lang, 'tools', 'src'),
);
}
/**
* Derive the target CodeQL CLI version from the extension's own
* package version. The base version (X.Y.Z, stripping any
* pre-release suffix like `-next.1`) corresponds to the CodeQL CLI
* release the VSIX was built against.
*/
getTargetCliVersion(): string {
const extensionVersion = this.serverManager.getExtensionVersion();
return PackInstaller.baseVersion(extensionVersion);
}
/**
* Look up the ql-mcp tools pack version to download for a given
* CodeQL CLI version.
*
* Returns the pack version string, or `undefined` if the CLI version
* has no known compatible pack release.
*/
static getPackVersionForCli(cliVersion: string): string | undefined {
const base = PackInstaller.baseVersion(cliVersion);
return PackInstaller.CLI_VERSION_TO_PACK_VERSION.get(base);
}
/**
* Install CodeQL pack dependencies for all (or specified) languages.
*
* When `downloadForCliVersion` is `true` (the default), the installer
* detects the actual CodeQL CLI version and, if it differs from what
* the VSIX was built against, downloads pre-compiled packs from GHCR
* for the matching CLI version. When the CLI version matches, or when
* downloading is disabled, falls back to `codeql pack install` on the
* bundled pack sources.
*/
async installAll(options?: PackInstallOptions & {
/** Download packs matching the detected CLI version (default: true). */
downloadForCliVersion?: boolean;
}): Promise<void> {
const codeqlPath = await this.cliResolver.resolve();
if (!codeqlPath) {
this.logger.warn(
'CodeQL CLI not found — skipping pack installation. Install the CLI or set CODEQL_PATH.',
);
return;
}
const languages =
options?.languages ?? [...PackInstaller.SUPPORTED_LANGUAGES];
const downloadEnabled = options?.downloadForCliVersion ?? true;
const actualCliVersion = this.cliResolver.getCliVersion();
const targetCliVersion = this.getTargetCliVersion();
if (actualCliVersion) {
this.logger.info(
`Detected CodeQL CLI version: ${actualCliVersion}, target: ${targetCliVersion}.`,
);
} else {
this.logger.info(
`CodeQL CLI version could not be determined. Target: ${targetCliVersion}. ` +
'Using bundled pack install.',
);
}
if (downloadEnabled && actualCliVersion && actualCliVersion !== targetCliVersion) {
this.logger.info(
`CodeQL CLI version ${actualCliVersion} differs from VSIX target ${targetCliVersion}. ` +
'Attempting to download compatible tool query packs...',
);
const downloaded = await this.downloadPacksForCliVersion(
codeqlPath, actualCliVersion, languages,
);
if (downloaded) {
return;
}
this.logger.info(
'Pack download did not succeed for all languages — falling back to bundled pack install.',
);
} else if (actualCliVersion && actualCliVersion === targetCliVersion) {
this.logger.info(
`CLI and target versions match (${actualCliVersion}). Using bundled pack install.`,
);
}
// Default path: install dependencies for bundled packs
await this.installBundledPacks(codeqlPath, languages);
}
/**
* Download pre-compiled tool query packs from GHCR for the specified
* CodeQL CLI version.
*
* Returns `true` if all requested languages were downloaded
* successfully, `false` otherwise.
*/
async downloadPacksForCliVersion(
codeqlPath: string,
cliVersion: string,
languages: string[],
): Promise<boolean> {
const packVersion = PackInstaller.getPackVersionForCli(cliVersion);
if (!packVersion) {
this.logger.warn(
`No known ql-mcp pack version for CodeQL CLI ${cliVersion}. ` +
'Falling back to bundled packs.',
);
return false;
}
this.logger.info(
`Downloading ql-mcp tool query packs v${packVersion} for CodeQL CLI ${cliVersion}...`,
);
let allSucceeded = true;
let successCount = 0;
for (const lang of languages) {
const packRef =
`${PackInstaller.PACK_SCOPE}/ql-mcp-${lang}-tools-src@${packVersion}`;
this.logger.info(`Downloading ${packRef}...`);
try {
await this.runCodeqlPackDownload(codeqlPath, packRef);
this.logger.info(`Downloaded ${packRef}.`);
successCount++;
} catch (err) {
this.logger.error(
`Failed to download ${packRef}: ${err instanceof Error ? err.message : String(err)}`,
);
allSucceeded = false;
}
}
this.logger.info(
`Pack download complete: ${successCount}/${languages.length} languages succeeded.`,
);
return allSucceeded;
}
/**
* Install pack dependencies for bundled packs using `codeql pack install`.
* This is the original behaviour and serves as the fallback when pack
* download is disabled or unavailable.
*/
private async installBundledPacks(
codeqlPath: string,
languages: string[],
): Promise<void> {
const qlRoot = this.getQlpackRoot();
let successCount = 0;
let skippedCount = 0;
for (const lang of languages) {
const packDir = join(qlRoot, 'ql', lang, 'tools', 'src');
const packName = `${PackInstaller.PACK_SCOPE}/ql-mcp-${lang}-tools-src`;
// Check if the pack directory exists
try {
await access(packDir, constants.R_OK);
} catch {
this.logger.debug(`Pack directory not found, skipping: ${packDir}`);
skippedCount++;
continue;
}
this.logger.info(`Installing CodeQL pack dependencies for ${packName} (${lang})...`);
try {
await this.runCodeqlPackInstall(codeqlPath, packDir);
this.logger.info(`Pack dependencies installed for ${packName} (${lang}).`);
successCount++;
} catch (err) {
this.logger.error(
`Failed to install pack dependencies for ${lang}: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
const attemptCount = languages.length - skippedCount;
const skippedSuffix = skippedCount > 0 ? `, ${skippedCount} skipped` : '';
this.logger.info(
`Bundled pack install complete: ${successCount}/${attemptCount} languages succeeded${skippedSuffix}.`,
);
}
/** Run `codeql pack install` for a single pack directory. */
private runCodeqlPackInstall(
codeqlPath: string,
packDir: string,
): Promise<void> {
return new Promise((resolve, reject) => {
execFile(
codeqlPath,
['pack', 'install', '--no-strict-mode', packDir],
{ timeout: 300_000 },
(err, _stdout, stderr) => {
if (err) {
reject(
new Error(`codeql pack install failed: ${stderr || err.message}`),
);
return;
}
resolve();
},
);
});
}
/** Run `codeql pack download` for a pack reference (e.g. scope/name@version). */
private runCodeqlPackDownload(
codeqlPath: string,
packRef: string,
): Promise<void> {
return new Promise((resolve, reject) => {
execFile(
codeqlPath,
['pack', 'download', packRef],
{ timeout: 300_000 },
(err, _stdout, stderr) => {
if (err) {
reject(
new Error(`codeql pack download failed: ${stderr || err.message}`),
);
return;
}
resolve();
},
);
});
}
/**
* Strip any pre-release suffix from a semver string, returning
* only the `MAJOR.MINOR.PATCH` portion.
*/
static baseVersion(version: string): string {
const stripped = version.startsWith('v') ? version.slice(1) : version;
const match = /^(\d+\.\d+\.\d+)/.exec(stripped);
return match ? match[1] : stripped;
}
}