@@ -25,6 +25,11 @@ export interface PackInstallOptions {
2525 * The bundled copy is always preferred so that the packs used by
2626 * `codeql pack install` match the server code running from the VSIX.
2727 *
28+ * When the detected CodeQL CLI version differs from the version the
29+ * VSIX was built against, the installer downloads pre-compiled packs
30+ * from GHCR for the matching CLI version via `codeql pack download`.
31+ * This ensures backwards compatibility across published CLI versions.
32+ *
2833 * CodeQL library dependencies (e.g. `codeql/javascript-all`) must be
2934 * fetched from GHCR via `codeql pack install`. This class automates
3035 * the `codeql-development-mcp-server-setup-packs` step documented in
@@ -43,6 +48,28 @@ export class PackInstaller extends DisposableObject {
4348 'swift' ,
4449 ] as const ;
4550
51+ /**
52+ * Maps CodeQL CLI versions to the ql-mcp tools pack version published
53+ * for that CLI release. Each ql-mcp stable release is built against a
54+ * specific CodeQL CLI version, and the published pack version matches
55+ * the ql-mcp release version.
56+ *
57+ * Compatibility range: v2.24.0 (initial public release) through the
58+ * current release.
59+ */
60+ static readonly CLI_VERSION_TO_PACK_VERSION : ReadonlyMap < string , string > =
61+ new Map ( [
62+ [ '2.24.0' , '2.24.0' ] ,
63+ [ '2.24.1' , '2.24.1' ] ,
64+ [ '2.24.2' , '2.24.2' ] ,
65+ [ '2.24.3' , '2.24.3' ] ,
66+ [ '2.25.0' , '2.25.0' ] ,
67+ [ '2.25.1' , '2.25.1' ] ,
68+ ] ) ;
69+
70+ /** Pack scope/prefix for all ql-mcp tools packs on GHCR. */
71+ static readonly PACK_SCOPE = 'advanced-security' ;
72+
4673 constructor (
4774 private readonly cliResolver : CliResolver ,
4875 private readonly serverManager : ServerManager ,
@@ -74,11 +101,43 @@ export class PackInstaller extends DisposableObject {
74101 ) ;
75102 }
76103
104+ /**
105+ * Derive the target CodeQL CLI version from the extension's own
106+ * package version. The base version (X.Y.Z, stripping any
107+ * pre-release suffix like `-next.1`) corresponds to the CodeQL CLI
108+ * release the VSIX was built against.
109+ */
110+ getTargetCliVersion ( ) : string {
111+ const extensionVersion = this . serverManager . getExtensionVersion ( ) ;
112+ return PackInstaller . baseVersion ( extensionVersion ) ;
113+ }
114+
115+ /**
116+ * Look up the ql-mcp tools pack version to download for a given
117+ * CodeQL CLI version.
118+ *
119+ * Returns the pack version string, or `undefined` if the CLI version
120+ * has no known compatible pack release.
121+ */
122+ static getPackVersionForCli ( cliVersion : string ) : string | undefined {
123+ const base = PackInstaller . baseVersion ( cliVersion ) ;
124+ return PackInstaller . CLI_VERSION_TO_PACK_VERSION . get ( base ) ;
125+ }
126+
77127 /**
78128 * Install CodeQL pack dependencies for all (or specified) languages.
79- * Requires the npm package to be installed locally first (via ServerManager).
129+ *
130+ * When `downloadForCliVersion` is `true` (the default), the installer
131+ * detects the actual CodeQL CLI version and, if it differs from what
132+ * the VSIX was built against, downloads pre-compiled packs from GHCR
133+ * for the matching CLI version. When the CLI version matches, or when
134+ * downloading is disabled, falls back to `codeql pack install` on the
135+ * bundled pack sources.
80136 */
81- async installAll ( options ?: PackInstallOptions ) : Promise < void > {
137+ async installAll ( options ?: PackInstallOptions & {
138+ /** Download packs matching the detected CLI version (default: true). */
139+ downloadForCliVersion ?: boolean ;
140+ } ) : Promise < void > {
82141 const codeqlPath = await this . cliResolver . resolve ( ) ;
83142 if ( ! codeqlPath ) {
84143 this . logger . warn (
@@ -87,10 +146,87 @@ export class PackInstaller extends DisposableObject {
87146 return ;
88147 }
89148
90- const qlRoot = this . getQlpackRoot ( ) ;
91149 const languages =
92150 options ?. languages ?? [ ...PackInstaller . SUPPORTED_LANGUAGES ] ;
93151
152+ const downloadEnabled = options ?. downloadForCliVersion ?? true ;
153+ const actualCliVersion = this . cliResolver . getCliVersion ( ) ;
154+ const targetCliVersion = this . getTargetCliVersion ( ) ;
155+
156+ if ( downloadEnabled && actualCliVersion && actualCliVersion !== targetCliVersion ) {
157+ this . logger . info (
158+ `CodeQL CLI version ${ actualCliVersion } differs from VSIX target ${ targetCliVersion } . ` +
159+ 'Attempting to download compatible tool query packs...' ,
160+ ) ;
161+ const downloaded = await this . downloadPacksForCliVersion (
162+ codeqlPath , actualCliVersion , languages ,
163+ ) ;
164+ if ( downloaded ) {
165+ return ;
166+ }
167+ this . logger . info (
168+ 'Pack download did not succeed for all languages — falling back to bundled pack install.' ,
169+ ) ;
170+ }
171+
172+ // Default path: install dependencies for bundled packs
173+ await this . installBundledPacks ( codeqlPath , languages ) ;
174+ }
175+
176+ /**
177+ * Download pre-compiled tool query packs from GHCR for the specified
178+ * CodeQL CLI version.
179+ *
180+ * Returns `true` if all requested languages were downloaded
181+ * successfully, `false` otherwise.
182+ */
183+ async downloadPacksForCliVersion (
184+ codeqlPath : string ,
185+ cliVersion : string ,
186+ languages : string [ ] ,
187+ ) : Promise < boolean > {
188+ const packVersion = PackInstaller . getPackVersionForCli ( cliVersion ) ;
189+ if ( ! packVersion ) {
190+ this . logger . warn (
191+ `No known ql-mcp pack version for CodeQL CLI ${ cliVersion } . ` +
192+ 'Falling back to bundled packs.' ,
193+ ) ;
194+ return false ;
195+ }
196+
197+ this . logger . info (
198+ `Downloading ql-mcp tool query packs v${ packVersion } for CodeQL CLI ${ cliVersion } ...` ,
199+ ) ;
200+
201+ let allSucceeded = true ;
202+ for ( const lang of languages ) {
203+ const packRef =
204+ `${ PackInstaller . PACK_SCOPE } /ql-mcp-${ lang } -tools-src@${ packVersion } ` ;
205+ this . logger . info ( `Downloading ${ packRef } ...` ) ;
206+ try {
207+ await this . runCodeqlPackDownload ( codeqlPath , packRef ) ;
208+ this . logger . info ( `Downloaded ${ packRef } .` ) ;
209+ } catch ( err ) {
210+ this . logger . error (
211+ `Failed to download ${ packRef } : ${ err instanceof Error ? err . message : String ( err ) } ` ,
212+ ) ;
213+ allSucceeded = false ;
214+ }
215+ }
216+ return allSucceeded ;
217+ }
218+
219+ /**
220+ * Install pack dependencies for bundled packs using `codeql pack install`.
221+ * This is the original behaviour and serves as the fallback when pack
222+ * download is disabled or unavailable.
223+ */
224+ private async installBundledPacks (
225+ codeqlPath : string ,
226+ languages : string [ ] ,
227+ ) : Promise < void > {
228+ const qlRoot = this . getQlpackRoot ( ) ;
229+
94230 for ( const lang of languages ) {
95231 const packDir = join ( qlRoot , 'ql' , lang , 'tools' , 'src' ) ;
96232
@@ -136,4 +272,37 @@ export class PackInstaller extends DisposableObject {
136272 ) ;
137273 } ) ;
138274 }
275+
276+ /** Run `codeql pack download` for a pack reference (e.g. scope/name@version). */
277+ private runCodeqlPackDownload (
278+ codeqlPath : string ,
279+ packRef : string ,
280+ ) : Promise < void > {
281+ return new Promise ( ( resolve , reject ) => {
282+ execFile (
283+ codeqlPath ,
284+ [ 'pack' , 'download' , packRef ] ,
285+ { timeout : 300_000 } ,
286+ ( err , _stdout , stderr ) => {
287+ if ( err ) {
288+ reject (
289+ new Error ( `codeql pack download failed: ${ stderr || err . message } ` ) ,
290+ ) ;
291+ return ;
292+ }
293+ resolve ( ) ;
294+ } ,
295+ ) ;
296+ } ) ;
297+ }
298+
299+ /**
300+ * Strip any pre-release suffix from a semver string, returning
301+ * only the `MAJOR.MINOR.PATCH` portion.
302+ */
303+ static baseVersion ( version : string ) : string {
304+ const stripped = version . startsWith ( 'v' ) ? version . slice ( 1 ) : version ;
305+ const match = / ^ ( \d + \. \d + \. \d + ) / . exec ( stripped ) ;
306+ return match ? match [ 1 ] : stripped ;
307+ }
139308}
0 commit comments