From 9cd4544c36b4df2411e9c764cc7668f1b9529305 Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 14:21:54 -0500 Subject: [PATCH 01/18] implement heuristic --- .../src/amazonq/lsp/workspaceInstaller.ts | 53 +++++++++++++--- packages/core/src/shared/lsp/lspResolver.ts | 4 +- packages/core/src/shared/utilities/tsUtils.ts | 9 +++ .../amazonq/lsp/workspaceinstaller.test.ts | 58 ++++++++++++++++++ .../amazonq/lsp/workspaceInstaller.test.ts | 61 +++++++++++++++++++ 5 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts create mode 100644 packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 6c5d869a70a..4ab8e756646 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -4,25 +4,33 @@ */ import path from 'path' -import { LspResolution, LspResolver } from '../../shared/lsp/types' +import { LspResolution, LspResolver, LspVersion } from '../../shared/lsp/types' import { ManifestResolver } from '../../shared/lsp/manifestResolver' import { LanguageServerResolver } from '../../shared/lsp/lspResolver' -import { Range } from 'semver' +import { Range, sort } from 'semver' import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' +import { partition } from '../../shared/utilities/tsUtils' const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions -const supportedLspServerVersions = '0.1.32' +export const supportedLspServerVersions = '0.1.32' +export const lspWorkspaceName = 'AmazonQ-Workspace' export class WorkspaceLSPResolver implements LspResolver { + public constructor( + private readonly options = { + versionRange: new Range(supportedLspServerVersions), + cleanUp: true, + } + ) {} + async resolve(): Promise { - const name = 'AmazonQ-Workspace' - const manifest = await new ManifestResolver(manifestUrl, name).resolve() + const manifest = await new ManifestResolver(manifestUrl, lspWorkspaceName).resolve() const installationResult = await new LanguageServerResolver( manifest, - name, - new Range(supportedLspServerVersions) + lspWorkspaceName, + this.options.versionRange ).resolve() const nodeName = @@ -30,7 +38,9 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - // TODO Cleanup old versions of language servers + if (this.options.cleanUp) { + await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) + } return { ...installationResult, resourcePaths: { @@ -42,4 +52,31 @@ export class WorkspaceLSPResolver implements LspResolver { }, } } + + private async getDownloadedVersions(downloadDirectory: string): Promise { + return (await fs.readdir(downloadDirectory)).map(([f, _], __) => f) + } + + private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { + return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false + } + + /** + * Delete all delisted versions and keep the two newest versions that remain + * @param manifest + * @param downloadDirectory + */ + async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise { + const downloadedVersions = await this.getDownloadedVersions(downloadDirectory) + const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => + this.isDelisted(manifestVersions, v) + ) + for (const v of delistedVersions) { + await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) + } + + for (const v of sort(remainingVersions).slice(0, -2)) { + await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) + } + } } diff --git a/packages/core/src/shared/lsp/lspResolver.ts b/packages/core/src/shared/lsp/lspResolver.ts index 7a6fc4102b0..1eafe24132e 100644 --- a/packages/core/src/shared/lsp/lspResolver.ts +++ b/packages/core/src/shared/lsp/lspResolver.ts @@ -14,6 +14,7 @@ import { getApplicationSupportFolder } from '../vscode/env' import { createHash } from '../crypto' import request from '../request' +export const langugeServerDefaultDir = path.join(getApplicationSupportFolder(), `aws/toolkits/language-servers`) export class LanguageServerResolver { constructor( private readonly manifest: Manifest, @@ -340,8 +341,7 @@ export class LanguageServerResolver { } defaultDownloadFolder() { - const applicationSupportFolder = getApplicationSupportFolder() - return path.join(applicationSupportFolder, `aws/toolkits/language-servers/${this.lsName}`) + return path.join(langugeServerDefaultDir, `${this.lsName}`) } private getDownloadDirectory(version: string) { diff --git a/packages/core/src/shared/utilities/tsUtils.ts b/packages/core/src/shared/utilities/tsUtils.ts index e4fbf5a2b3f..22fb07b48de 100644 --- a/packages/core/src/shared/utilities/tsUtils.ts +++ b/packages/core/src/shared/utilities/tsUtils.ts @@ -94,6 +94,15 @@ export function createFactoryFunction any>(cto return (...args) => new ctor(...args) } +export function partition(lst: T[], pred: (arg: T) => boolean): [T[], T[]] { + return lst.reduce( + ([leftAcc, rightAcc], item) => { + return pred(item) ? [[...leftAcc, item], rightAcc] : [leftAcc, [...rightAcc, item]] + }, + [[], []] as [T[], T[]] + ) +} + type NoSymbols = { [Property in keyof T]: Property extends symbol ? never : Property }[keyof T] export type InterfaceNoSymbol = Pick> /** diff --git a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts new file mode 100644 index 00000000000..dec05cff657 --- /dev/null +++ b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts @@ -0,0 +1,58 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Uri } from 'vscode' +import { fs } from '../../../shared' +import { createTestWorkspaceFolder } from '../../testUtil' +import path from 'path' +import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' +import assert from 'assert' + +async function fakeInstallVersion(version: string, installationDir: string): Promise { + const versionDir = path.join(installationDir, version) + await fs.mkdir(versionDir) + await fs.writeFile(path.join(versionDir, 'file.txt'), 'content') +} + +describe('workspaceInstaller', function () { + describe('cleanUp', function () { + let installationDir: Uri + let versions: string[] + + before(async function () { + installationDir = (await createTestWorkspaceFolder()).uri + versions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1'] + }) + + beforeEach(async function () { + for (const v of versions) { + await fakeInstallVersion(v, installationDir.fsPath) + } + }) + + after(async function () { + await fs.delete(installationDir, { force: true, recursive: true }) + }) + it('keeps two newest versions', async function () { + const wsr = new WorkspaceLSPResolver() + await wsr.cleanUp([], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 2) + assert.ok(result.includes('2.1.1')) + assert.ok(result.includes('1.1.1')) + }) + + it('deletes delisted versions', async function () { + const wsr = new WorkspaceLSPResolver() + await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 2) + assert.ok(result.includes('2.1.1')) + assert.ok(result.includes('1.0.1')) + }) + }) +}) diff --git a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts new file mode 100644 index 00000000000..0e38a7fde5f --- /dev/null +++ b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts @@ -0,0 +1,61 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Range } from 'semver' +import assert from 'assert' +import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' +import { fs } from '../../../shared/fs/fs' +import path from 'path' +import * as sinon from 'sinon' +import { langugeServerDefaultDir } from '../../../shared/lsp/lspResolver' + +async function installVersion(version: string, cleanUp: boolean = false) { + const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp }) + return await resolver.resolve() +} + +/** + * Installs all versions, only running 'cleanUp' on the last install. + * @param versions + * @returns + */ +async function testInstallVersions(versions: string[]) { + await Promise.all(versions.slice(0, -1).map(async (version) => await installVersion(version))) + const finalVersionResult = await installVersion(versions[versions.length - 1], true) + const allVersions = path.dirname(finalVersionResult.assetDirectory) + const versionsDownloaded = (await fs.readdir(allVersions)).map(([f, _], __) => f) + return versionsDownloaded +} + +describe('workspaceInstaller', function () { + before(async function () { + await fs.delete(langugeServerDefaultDir, { force: true, recursive: true }) + }) + + it('removes all but the latest two versions', async function () { + const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28'] + const versionsDownloaded = await testInstallVersions(versionsToInstall) + + assert.strictEqual(versionsDownloaded.length, 2) + assert.ok(versionsDownloaded.includes('0.1.28')) + assert.ok(versionsDownloaded.includes('0.1.29')) + }) + + it('removes delisted versions then keeps 2 remaining most recent', async function () { + const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any) + isDelisted.callsFake((_manifestVersions, version) => { + return version === '0.1.27' || version === '0.1.29' + }) + + const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28'] + const versionsDownloaded = await testInstallVersions(versionsToInstall) + + console.log(versionsDownloaded) + assert.strictEqual(versionsDownloaded.length, 2) + assert.ok(versionsDownloaded.includes('0.1.28')) + assert.ok(versionsDownloaded.includes('0.1.26')) + isDelisted.restore() + }) +}) From e7bf0af8cc8c57a67d46ce572d3a988c90c76f15 Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 14:26:46 -0500 Subject: [PATCH 02/18] clean up handling on optional args --- .../src/amazonq/lsp/workspaceInstaller.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 4ab8e756646..09b8b54ee28 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -18,19 +18,24 @@ export const supportedLspServerVersions = '0.1.32' export const lspWorkspaceName = 'AmazonQ-Workspace' export class WorkspaceLSPResolver implements LspResolver { + private readonly versionRange: Range + private readonly shouldCleanUp: boolean public constructor( - private readonly options = { - versionRange: new Range(supportedLspServerVersions), - cleanUp: true, - } - ) {} + options?: Partial<{ + versionRange: Range + cleanUp: boolean + }> + ) { + this.versionRange = options?.versionRange ?? new Range(supportedLspServerVersions) + this.shouldCleanUp = options?.cleanUp ?? true + } async resolve(): Promise { const manifest = await new ManifestResolver(manifestUrl, lspWorkspaceName).resolve() const installationResult = await new LanguageServerResolver( manifest, lspWorkspaceName, - this.options.versionRange + this.versionRange ).resolve() const nodeName = @@ -38,7 +43,7 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - if (this.options.cleanUp) { + if (this.shouldCleanUp) { await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) } return { From 90d23b1aa64c8312d338f4a604b1cb9e710ba494 Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 14:31:59 -0500 Subject: [PATCH 03/18] add tests for utility method --- .../src/test/shared/utilities/tsUtils.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/core/src/test/shared/utilities/tsUtils.test.ts diff --git a/packages/core/src/test/shared/utilities/tsUtils.test.ts b/packages/core/src/test/shared/utilities/tsUtils.test.ts new file mode 100644 index 00000000000..eb04da035e5 --- /dev/null +++ b/packages/core/src/test/shared/utilities/tsUtils.test.ts @@ -0,0 +1,16 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { partition } from '../../../shared/utilities/tsUtils' +import assert from 'assert' + +describe('partition', function () { + it('should split the list according to predicate', function () { + const items = [1, 2, 3, 4, 5, 6, 7, 8] + const [even, odd] = partition(items, (i) => i % 2 === 0) + assert.deepStrictEqual(even, [2, 4, 6, 8]) + assert.deepStrictEqual(odd, [1, 3, 5, 7]) + }) +}) From 2c19cf4b6454e7db23825dea285a1b8fddb1b635 Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 14:48:52 -0500 Subject: [PATCH 04/18] fetch versions from manifest file --- .../src/amazonq/lsp/workspaceInstaller.ts | 4 +-- .../amazonq/lsp/workspaceInstaller.test.ts | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 09b8b54ee28..7018958a3ee 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -12,7 +12,7 @@ import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' import { partition } from '../../shared/utilities/tsUtils' -const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' +export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions export const supportedLspServerVersions = '0.1.32' export const lspWorkspaceName = 'AmazonQ-Workspace' @@ -31,7 +31,7 @@ export class WorkspaceLSPResolver implements LspResolver { } async resolve(): Promise { - const manifest = await new ManifestResolver(manifestUrl, lspWorkspaceName).resolve() + const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() const installationResult = await new LanguageServerResolver( manifest, lspWorkspaceName, diff --git a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts index 0e38a7fde5f..8f523ec8d28 100644 --- a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts +++ b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Range } from 'semver' +import { Range, sort } from 'semver' import assert from 'assert' -import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' +import { lspWorkspaceName, lspManifestUrl, WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' import { fs } from '../../../shared/fs/fs' import path from 'path' import * as sinon from 'sinon' import { langugeServerDefaultDir } from '../../../shared/lsp/lspResolver' +import { ManifestResolver } from '../../../shared' async function installVersion(version: string, cleanUp: boolean = false) { const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp }) @@ -30,32 +31,37 @@ async function testInstallVersions(versions: string[]) { } describe('workspaceInstaller', function () { + let testVersions: string[] before(async function () { await fs.delete(langugeServerDefaultDir, { force: true, recursive: true }) + const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() + testVersions = sort( + manifest.versions + .filter((v) => !v.isDelisted) + .slice(0, 4) + .map((v) => v.serverVersion) + ) }) it('removes all but the latest two versions', async function () { - const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28'] - const versionsDownloaded = await testInstallVersions(versionsToInstall) + const versionsDownloaded = await testInstallVersions(testVersions) assert.strictEqual(versionsDownloaded.length, 2) - assert.ok(versionsDownloaded.includes('0.1.28')) - assert.ok(versionsDownloaded.includes('0.1.29')) + assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) + assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 2])) }) it('removes delisted versions then keeps 2 remaining most recent', async function () { const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any) isDelisted.callsFake((_manifestVersions, version) => { - return version === '0.1.27' || version === '0.1.29' + return version === testVersions[testVersions.length - 2] }) - const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28'] - const versionsDownloaded = await testInstallVersions(versionsToInstall) + const versionsDownloaded = await testInstallVersions(testVersions) - console.log(versionsDownloaded) assert.strictEqual(versionsDownloaded.length, 2) - assert.ok(versionsDownloaded.includes('0.1.28')) - assert.ok(versionsDownloaded.includes('0.1.26')) + assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) + assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 3])) isDelisted.restore() }) }) From 1c366ef43e8d2dff093c738e59a64d2f9354280e Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 15:07:17 -0500 Subject: [PATCH 05/18] skip integ tests on non-mac --- packages/core/src/shared/lsp/lspResolver.ts | 8 ++++++-- .../amazonq/lsp/workspaceInstaller.test.ts | 14 +++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/core/src/shared/lsp/lspResolver.ts b/packages/core/src/shared/lsp/lspResolver.ts index 1eafe24132e..a471b0e4546 100644 --- a/packages/core/src/shared/lsp/lspResolver.ts +++ b/packages/core/src/shared/lsp/lspResolver.ts @@ -14,7 +14,6 @@ import { getApplicationSupportFolder } from '../vscode/env' import { createHash } from '../crypto' import request from '../request' -export const langugeServerDefaultDir = path.join(getApplicationSupportFolder(), `aws/toolkits/language-servers`) export class LanguageServerResolver { constructor( private readonly manifest: Manifest, @@ -340,8 +339,13 @@ export class LanguageServerResolver { return version.targets.find((x) => x.arch === arch && x.platform === platform) } + // lazy calls to `getApplicationSupportFolder()` to avoid failure on windows. + public static get defaultDir() { + return path.join(getApplicationSupportFolder(), `aws/toolkits/language-servers`) + } + defaultDownloadFolder() { - return path.join(langugeServerDefaultDir, `${this.lsName}`) + return path.join(LanguageServerResolver.defaultDir, `${this.lsName}`) } private getDownloadDirectory(version: string) { diff --git a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts index 8f523ec8d28..a22b51961e8 100644 --- a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts +++ b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts @@ -9,8 +9,7 @@ import { lspWorkspaceName, lspManifestUrl, WorkspaceLSPResolver } from '../../.. import { fs } from '../../../shared/fs/fs' import path from 'path' import * as sinon from 'sinon' -import { langugeServerDefaultDir } from '../../../shared/lsp/lspResolver' -import { ManifestResolver } from '../../../shared' +import { LanguageServerResolver, ManifestResolver } from '../../../shared' async function installVersion(version: string, cleanUp: boolean = false) { const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp }) @@ -32,8 +31,11 @@ async function testInstallVersions(versions: string[]) { describe('workspaceInstaller', function () { let testVersions: string[] + let onMac: boolean before(async function () { - await fs.delete(langugeServerDefaultDir, { force: true, recursive: true }) + // TODO: remove this when non-mac support is added. + onMac = process.platform === 'darwin' + await fs.delete(LanguageServerResolver.defaultDir, { force: true, recursive: true }) const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() testVersions = sort( manifest.versions @@ -44,6 +46,9 @@ describe('workspaceInstaller', function () { }) it('removes all but the latest two versions', async function () { + if (!onMac) { + this.skip() + } const versionsDownloaded = await testInstallVersions(testVersions) assert.strictEqual(versionsDownloaded.length, 2) @@ -52,6 +57,9 @@ describe('workspaceInstaller', function () { }) it('removes delisted versions then keeps 2 remaining most recent', async function () { + if (!onMac) { + this.skip() + } const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any) isDelisted.callsFake((_manifestVersions, version) => { return version === testVersions[testVersions.length - 2] From b00ec0de3f3c6c8c87ec978d82db4c486ed98990 Mon Sep 17 00:00:00 2001 From: hkobew Date: Tue, 21 Jan 2025 16:02:20 -0500 Subject: [PATCH 06/18] skip beforeAll hook on non-mac --- .../core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts index a22b51961e8..9c6c1958896 100644 --- a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts +++ b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts @@ -35,6 +35,9 @@ describe('workspaceInstaller', function () { before(async function () { // TODO: remove this when non-mac support is added. onMac = process.platform === 'darwin' + if (!onMac) { + this.skip() + } await fs.delete(LanguageServerResolver.defaultDir, { force: true, recursive: true }) const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() testVersions = sort( From 41986117252a6fdcf4827a05c49c1301a67f200d Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:00:23 -0500 Subject: [PATCH 07/18] pull out helper function so that it can be used by other modules --- packages/core/src/amazonq/lsp/util.ts | 10 ++++++++++ packages/core/src/amazonq/lsp/workspaceInstaller.ts | 7 ++----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/amazonq/lsp/util.ts diff --git a/packages/core/src/amazonq/lsp/util.ts b/packages/core/src/amazonq/lsp/util.ts new file mode 100644 index 00000000000..c752079b776 --- /dev/null +++ b/packages/core/src/amazonq/lsp/util.ts @@ -0,0 +1,10 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fs } from '../../shared/fs/fs' + +export async function getDownloadedVersions(installLocation: string) { + return (await fs.readdir(installLocation)).map(([f, _], __) => f) +} diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 7018958a3ee..9ab83227d38 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -11,6 +11,7 @@ import { Range, sort } from 'semver' import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' import { partition } from '../../shared/utilities/tsUtils' +import { getDownloadedVersions } from './util' export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions @@ -58,10 +59,6 @@ export class WorkspaceLSPResolver implements LspResolver { } } - private async getDownloadedVersions(downloadDirectory: string): Promise { - return (await fs.readdir(downloadDirectory)).map(([f, _], __) => f) - } - private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false } @@ -72,7 +69,7 @@ export class WorkspaceLSPResolver implements LspResolver { * @param downloadDirectory */ async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise { - const downloadedVersions = await this.getDownloadedVersions(downloadDirectory) + const downloadedVersions = await getDownloadedVersions(downloadDirectory) const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => this.isDelisted(manifestVersions, v) ) From 32d2ba39b9eeb98d80edc27b0da59b6c306de128 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:06:45 -0500 Subject: [PATCH 08/18] handle case where remaining versions is 2 or less --- .../src/amazonq/lsp/workspaceInstaller.ts | 4 ++++ .../amazonq/lsp/workspaceinstaller.test.ts | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 9ab83227d38..f5177e50e90 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -77,6 +77,10 @@ export class WorkspaceLSPResolver implements LspResolver { await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) } + if (remainingVersions.length <= 2) { + return + } + for (const v of sort(remainingVersions).slice(0, -2)) { await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) } diff --git a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts index dec05cff657..444218e4f40 100644 --- a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts +++ b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts @@ -19,15 +19,15 @@ async function fakeInstallVersion(version: string, installationDir: string): Pro describe('workspaceInstaller', function () { describe('cleanUp', function () { let installationDir: Uri - let versions: string[] + let lspVersions: string[] before(async function () { installationDir = (await createTestWorkspaceFolder()).uri - versions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1'] + lspVersions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1'] }) beforeEach(async function () { - for (const v of versions) { + for (const v of lspVersions) { await fakeInstallVersion(v, installationDir.fsPath) } }) @@ -54,5 +54,21 @@ describe('workspaceInstaller', function () { assert.ok(result.includes('2.1.1')) assert.ok(result.includes('1.0.1')) }) + + it('handles case where less than 2 versions are not delisted', async function () { + const wsr = new WorkspaceLSPResolver() + await wsr.cleanUp( + [ + { serverVersion: '1.1.1', isDelisted: true, targets: [] }, + { serverVersion: '2.1.1', isDelisted: true, targets: [] }, + { serverVersion: '1.0.0', isDelisted: true, targets: [] }, + ], + installationDir.fsPath + ) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 1) + assert.ok(result.includes('1.0.1')) + }) }) }) From a7a89c13ae1e80f3c93d2601484195f5c1dd5599 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:10:39 -0500 Subject: [PATCH 09/18] add comment to partition helper function --- packages/core/src/shared/utilities/tsUtils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/shared/utilities/tsUtils.ts b/packages/core/src/shared/utilities/tsUtils.ts index 22fb07b48de..2e4838c5b7a 100644 --- a/packages/core/src/shared/utilities/tsUtils.ts +++ b/packages/core/src/shared/utilities/tsUtils.ts @@ -94,6 +94,12 @@ export function createFactoryFunction any>(cto return (...args) => new ctor(...args) } +/** + * Split a list into two sublists based on the result of a predicate. + * @param lst list to split + * @param pred predicate to apply to each element + * @returns two nested lists, where for all items x in the left sublist, pred(x) returns true. The remaining elements are in the right sublist. + */ export function partition(lst: T[], pred: (arg: T) => boolean): [T[], T[]] { return lst.reduce( ([leftAcc, rightAcc], item) => { From ee7343b3957df24858ef549cc307fcd424ac3d56 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:12:39 -0500 Subject: [PATCH 10/18] remove special logic for integ tests --- .../src/amazonq/lsp/workspaceInstaller.ts | 18 +---- .../amazonq/lsp/workspaceInstaller.test.ts | 78 ------------------- 2 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index f5177e50e90..33bb908ebeb 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -19,24 +19,12 @@ export const supportedLspServerVersions = '0.1.32' export const lspWorkspaceName = 'AmazonQ-Workspace' export class WorkspaceLSPResolver implements LspResolver { - private readonly versionRange: Range - private readonly shouldCleanUp: boolean - public constructor( - options?: Partial<{ - versionRange: Range - cleanUp: boolean - }> - ) { - this.versionRange = options?.versionRange ?? new Range(supportedLspServerVersions) - this.shouldCleanUp = options?.cleanUp ?? true - } - async resolve(): Promise { const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() const installationResult = await new LanguageServerResolver( manifest, lspWorkspaceName, - this.versionRange + new Range(supportedLspServerVersions) ).resolve() const nodeName = @@ -44,9 +32,7 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - if (this.shouldCleanUp) { - await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) - } + await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: { diff --git a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts b/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts deleted file mode 100644 index 9c6c1958896..00000000000 --- a/packages/core/src/testInteg/amazonq/lsp/workspaceInstaller.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { Range, sort } from 'semver' -import assert from 'assert' -import { lspWorkspaceName, lspManifestUrl, WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' -import { fs } from '../../../shared/fs/fs' -import path from 'path' -import * as sinon from 'sinon' -import { LanguageServerResolver, ManifestResolver } from '../../../shared' - -async function installVersion(version: string, cleanUp: boolean = false) { - const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp }) - return await resolver.resolve() -} - -/** - * Installs all versions, only running 'cleanUp' on the last install. - * @param versions - * @returns - */ -async function testInstallVersions(versions: string[]) { - await Promise.all(versions.slice(0, -1).map(async (version) => await installVersion(version))) - const finalVersionResult = await installVersion(versions[versions.length - 1], true) - const allVersions = path.dirname(finalVersionResult.assetDirectory) - const versionsDownloaded = (await fs.readdir(allVersions)).map(([f, _], __) => f) - return versionsDownloaded -} - -describe('workspaceInstaller', function () { - let testVersions: string[] - let onMac: boolean - before(async function () { - // TODO: remove this when non-mac support is added. - onMac = process.platform === 'darwin' - if (!onMac) { - this.skip() - } - await fs.delete(LanguageServerResolver.defaultDir, { force: true, recursive: true }) - const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() - testVersions = sort( - manifest.versions - .filter((v) => !v.isDelisted) - .slice(0, 4) - .map((v) => v.serverVersion) - ) - }) - - it('removes all but the latest two versions', async function () { - if (!onMac) { - this.skip() - } - const versionsDownloaded = await testInstallVersions(testVersions) - - assert.strictEqual(versionsDownloaded.length, 2) - assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) - assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 2])) - }) - - it('removes delisted versions then keeps 2 remaining most recent', async function () { - if (!onMac) { - this.skip() - } - const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any) - isDelisted.callsFake((_manifestVersions, version) => { - return version === testVersions[testVersions.length - 2] - }) - - const versionsDownloaded = await testInstallVersions(testVersions) - - assert.strictEqual(versionsDownloaded.length, 2) - assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 1])) - assert.ok(versionsDownloaded.includes(testVersions[testVersions.length - 3])) - isDelisted.restore() - }) -}) From 604d2e1717e57f9884789c3565e0f2d3fc5ff572 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:28:09 -0500 Subject: [PATCH 11/18] increase test coverage for cases when < 2 versions --- .../amazonq/lsp/workspaceinstaller.test.ts | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts index 444218e4f40..c0b75b3cf72 100644 --- a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts +++ b/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts @@ -16,26 +16,33 @@ async function fakeInstallVersion(version: string, installationDir: string): Pro await fs.writeFile(path.join(versionDir, 'file.txt'), 'content') } +async function fakeInstallVersions(versions: string[], installationDir: string): Promise { + for (const v of versions) { + await fakeInstallVersion(v, installationDir) + } +} + describe('workspaceInstaller', function () { describe('cleanUp', function () { let installationDir: Uri - let lspVersions: string[] before(async function () { installationDir = (await createTestWorkspaceFolder()).uri - lspVersions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1'] }) - beforeEach(async function () { - for (const v of lspVersions) { - await fakeInstallVersion(v, installationDir.fsPath) + afterEach(async function () { + const files = await fs.readdir(installationDir.fsPath) + for (const [name, _type] of files) { + await fs.delete(path.join(installationDir.fsPath, name), { force: true, recursive: true }) } }) after(async function () { await fs.delete(installationDir, { force: true, recursive: true }) }) + it('keeps two newest versions', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) const wsr = new WorkspaceLSPResolver() await wsr.cleanUp([], installationDir.fsPath) @@ -46,6 +53,7 @@ describe('workspaceInstaller', function () { }) it('deletes delisted versions', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) const wsr = new WorkspaceLSPResolver() await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) @@ -56,6 +64,7 @@ describe('workspaceInstaller', function () { }) it('handles case where less than 2 versions are not delisted', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) const wsr = new WorkspaceLSPResolver() await wsr.cleanUp( [ @@ -70,5 +79,23 @@ describe('workspaceInstaller', function () { assert.strictEqual(result.length, 1) assert.ok(result.includes('1.0.1')) }) + + it('handles case where less than 2 versions exist', async function () { + await fakeInstallVersions(['1.0.0'], installationDir.fsPath) + const wsr = new WorkspaceLSPResolver() + await wsr.cleanUp([], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 1) + }) + + it('does not install delisted version when no other option exists', async function () { + await fakeInstallVersions(['1.0.0'], installationDir.fsPath) + const wsr = new WorkspaceLSPResolver() + await wsr.cleanUp([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 0) + }) }) }) From 78aae8f4d09ce65ba8b366b6a14fc6689366c1ab Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 22 Jan 2025 10:29:21 -0500 Subject: [PATCH 12/18] rename file --- .../{workspaceinstaller.test.ts => workplaceInstaller.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/core/src/test/amazonq/lsp/{workspaceinstaller.test.ts => workplaceInstaller.test.ts} (100%) diff --git a/packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts b/packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts similarity index 100% rename from packages/core/src/test/amazonq/lsp/workspaceinstaller.test.ts rename to packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts From 3b693c6550e79160350d9ad7442e5d522367c2af Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 12:48:39 -0500 Subject: [PATCH 13/18] fix: generalize cleanup to apply to other installer --- packages/amazonq/src/lsp/lspInstaller.ts | 3 +- packages/core/src/amazonq/index.ts | 1 + packages/core/src/amazonq/lsp/util.ts | 31 ++++++ .../src/amazonq/lsp/workspaceInstaller.ts | 36 +------ .../core/src/test/amazonq/lsp/util.test.ts | 94 ++++++++++++++++ .../amazonq/lsp/workplaceInstaller.test.ts | 101 ------------------ 6 files changed, 132 insertions(+), 134 deletions(-) create mode 100644 packages/core/src/test/amazonq/lsp/util.test.ts delete mode 100644 packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index a087abf395c..1218b00751d 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -14,6 +14,7 @@ import { getNodeExecutableName, } from 'aws-core-vscode/shared' import path from 'path' +import { cleanUpLSPDownloads } from 'aws-core-vscode/amazonq' const manifestURL = 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json' export const supportedLspServerVersions = '^2.3.0' @@ -46,7 +47,7 @@ export class AmazonQLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, `servers/${getNodeExecutableName()}`) await fs.chmod(nodePath, 0o755) - // TODO Cleanup old versions of language servers + await cleanUpLSPDownloads(manifest.versions, path.dirname(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: { diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index c5abbf7658e..1ad208914ee 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -17,6 +17,7 @@ export { } from './onboardingPage/walkthrough' export { LspController } from './lsp/lspController' export { LspClient } from './lsp/lspClient' +export { cleanUpLSPDownloads } from './lsp/util' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' export { init as cwChatAppInit } from '../codewhispererChat/app' diff --git a/packages/core/src/amazonq/lsp/util.ts b/packages/core/src/amazonq/lsp/util.ts index c752079b776..75546bd5b1c 100644 --- a/packages/core/src/amazonq/lsp/util.ts +++ b/packages/core/src/amazonq/lsp/util.ts @@ -3,8 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ +import path from 'path' +import { LspVersion } from '../../shared' import { fs } from '../../shared/fs/fs' +import { partition } from '../../shared/utilities/tsUtils' +import { sort } from 'semver' export async function getDownloadedVersions(installLocation: string) { return (await fs.readdir(installLocation)).map(([f, _], __) => f) } + +function isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { + return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false +} + +/** + * Delete all delisted versions and keep the two newest versions that remain + * @param manifest + * @param downloadDirectory + */ +export async function cleanUpLSPDownloads(manifestVersions: LspVersion[], downloadDirectory: string): Promise { + const downloadedVersions = await getDownloadedVersions(downloadDirectory) + const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => + isDelisted(manifestVersions, v) + ) + for (const v of delistedVersions) { + await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) + } + + if (remainingVersions.length <= 2) { + return + } + + for (const v of sort(remainingVersions).slice(0, -2)) { + await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) + } +} diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 33bb908ebeb..6162780aaeb 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -4,14 +4,13 @@ */ import path from 'path' -import { LspResolution, LspResolver, LspVersion } from '../../shared/lsp/types' +import { LspResolution, LspResolver } from '../../shared/lsp/types' import { ManifestResolver } from '../../shared/lsp/manifestResolver' import { LanguageServerResolver } from '../../shared/lsp/lspResolver' -import { Range, sort } from 'semver' +import { Range } from 'semver' import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' -import { partition } from '../../shared/utilities/tsUtils' -import { getDownloadedVersions } from './util' +import { cleanUpLSPDownloads } from './util' export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions @@ -32,7 +31,7 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory)) + await cleanUpLSPDownloads(manifest.versions, path.dirname(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: { @@ -44,31 +43,4 @@ export class WorkspaceLSPResolver implements LspResolver { }, } } - - private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { - return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false - } - - /** - * Delete all delisted versions and keep the two newest versions that remain - * @param manifest - * @param downloadDirectory - */ - async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise { - const downloadedVersions = await getDownloadedVersions(downloadDirectory) - const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => - this.isDelisted(manifestVersions, v) - ) - for (const v of delistedVersions) { - await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) - } - - if (remainingVersions.length <= 2) { - return - } - - for (const v of sort(remainingVersions).slice(0, -2)) { - await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) - } - } } diff --git a/packages/core/src/test/amazonq/lsp/util.test.ts b/packages/core/src/test/amazonq/lsp/util.test.ts new file mode 100644 index 00000000000..22ea6bbed52 --- /dev/null +++ b/packages/core/src/test/amazonq/lsp/util.test.ts @@ -0,0 +1,94 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Uri } from 'vscode' +import { fs } from '../../../shared' +import { createTestWorkspaceFolder } from '../../testUtil' +import path from 'path' +import assert from 'assert' +import { cleanUpLSPDownloads } from '../../../amazonq/lsp/util' + +async function fakeInstallVersion(version: string, installationDir: string): Promise { + const versionDir = path.join(installationDir, version) + await fs.mkdir(versionDir) + await fs.writeFile(path.join(versionDir, 'file.txt'), 'content') +} + +async function fakeInstallVersions(versions: string[], installationDir: string): Promise { + for (const v of versions) { + await fakeInstallVersion(v, installationDir) + } +} + +describe('cleanUpLSPDownloads', function () { + let installationDir: Uri + + before(async function () { + installationDir = (await createTestWorkspaceFolder()).uri + }) + + afterEach(async function () { + const files = await fs.readdir(installationDir.fsPath) + for (const [name, _type] of files) { + await fs.delete(path.join(installationDir.fsPath, name), { force: true, recursive: true }) + } + }) + + after(async function () { + await fs.delete(installationDir, { force: true, recursive: true }) + }) + + it('keeps two newest versions', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) + await cleanUpLSPDownloads([], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 2) + assert.ok(result.includes('2.1.1')) + assert.ok(result.includes('1.1.1')) + }) + + it('deletes delisted versions', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) + await cleanUpLSPDownloads([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 2) + assert.ok(result.includes('2.1.1')) + assert.ok(result.includes('1.0.1')) + }) + + it('handles case where less than 2 versions are not delisted', async function () { + await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) + await cleanUpLSPDownloads( + [ + { serverVersion: '1.1.1', isDelisted: true, targets: [] }, + { serverVersion: '2.1.1', isDelisted: true, targets: [] }, + { serverVersion: '1.0.0', isDelisted: true, targets: [] }, + ], + installationDir.fsPath + ) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 1) + assert.ok(result.includes('1.0.1')) + }) + + it('handles case where less than 2 versions exist', async function () { + await fakeInstallVersions(['1.0.0'], installationDir.fsPath) + await cleanUpLSPDownloads([], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 1) + }) + + it('does not install delisted version when no other option exists', async function () { + await fakeInstallVersions(['1.0.0'], installationDir.fsPath) + await cleanUpLSPDownloads([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath) + + const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) + assert.strictEqual(result.length, 0) + }) +}) diff --git a/packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts b/packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts deleted file mode 100644 index c0b75b3cf72..00000000000 --- a/packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { Uri } from 'vscode' -import { fs } from '../../../shared' -import { createTestWorkspaceFolder } from '../../testUtil' -import path from 'path' -import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller' -import assert from 'assert' - -async function fakeInstallVersion(version: string, installationDir: string): Promise { - const versionDir = path.join(installationDir, version) - await fs.mkdir(versionDir) - await fs.writeFile(path.join(versionDir, 'file.txt'), 'content') -} - -async function fakeInstallVersions(versions: string[], installationDir: string): Promise { - for (const v of versions) { - await fakeInstallVersion(v, installationDir) - } -} - -describe('workspaceInstaller', function () { - describe('cleanUp', function () { - let installationDir: Uri - - before(async function () { - installationDir = (await createTestWorkspaceFolder()).uri - }) - - afterEach(async function () { - const files = await fs.readdir(installationDir.fsPath) - for (const [name, _type] of files) { - await fs.delete(path.join(installationDir.fsPath, name), { force: true, recursive: true }) - } - }) - - after(async function () { - await fs.delete(installationDir, { force: true, recursive: true }) - }) - - it('keeps two newest versions', async function () { - await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - const wsr = new WorkspaceLSPResolver() - await wsr.cleanUp([], installationDir.fsPath) - - const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) - assert.strictEqual(result.length, 2) - assert.ok(result.includes('2.1.1')) - assert.ok(result.includes('1.1.1')) - }) - - it('deletes delisted versions', async function () { - await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - const wsr = new WorkspaceLSPResolver() - await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) - - const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) - assert.strictEqual(result.length, 2) - assert.ok(result.includes('2.1.1')) - assert.ok(result.includes('1.0.1')) - }) - - it('handles case where less than 2 versions are not delisted', async function () { - await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - const wsr = new WorkspaceLSPResolver() - await wsr.cleanUp( - [ - { serverVersion: '1.1.1', isDelisted: true, targets: [] }, - { serverVersion: '2.1.1', isDelisted: true, targets: [] }, - { serverVersion: '1.0.0', isDelisted: true, targets: [] }, - ], - installationDir.fsPath - ) - - const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) - assert.strictEqual(result.length, 1) - assert.ok(result.includes('1.0.1')) - }) - - it('handles case where less than 2 versions exist', async function () { - await fakeInstallVersions(['1.0.0'], installationDir.fsPath) - const wsr = new WorkspaceLSPResolver() - await wsr.cleanUp([], installationDir.fsPath) - - const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) - assert.strictEqual(result.length, 1) - }) - - it('does not install delisted version when no other option exists', async function () { - await fakeInstallVersions(['1.0.0'], installationDir.fsPath) - const wsr = new WorkspaceLSPResolver() - await wsr.cleanUp([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath) - - const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) - assert.strictEqual(result.length, 0) - }) - }) -}) From d5b972eb2703339aeb749b3f222ce19a9dc0360c Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 12:51:29 -0500 Subject: [PATCH 14/18] refactor: avoid exporting unnecessarily --- packages/core/src/amazonq/lsp/workspaceInstaller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 6162780aaeb..2545267150c 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -12,10 +12,10 @@ import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' import { cleanUpLSPDownloads } from './util' -export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' +const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions -export const supportedLspServerVersions = '0.1.32' -export const lspWorkspaceName = 'AmazonQ-Workspace' +const supportedLspServerVersions = '0.1.32' +const lspWorkspaceName = 'AmazonQ-Workspace' export class WorkspaceLSPResolver implements LspResolver { async resolve(): Promise { From bf36a48239c698093ffe1c01d11de7576b099158 Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 13:01:05 -0500 Subject: [PATCH 15/18] refactor: reduce noise in changes --- packages/core/src/amazonq/lsp/workspaceInstaller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 2545267150c..845d7d1c47c 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -12,17 +12,17 @@ import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' import { cleanUpLSPDownloads } from './util' -const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' +const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions const supportedLspServerVersions = '0.1.32' -const lspWorkspaceName = 'AmazonQ-Workspace' export class WorkspaceLSPResolver implements LspResolver { async resolve(): Promise { - const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve() + const name = 'AmazonQ-Workspace' + const manifest = await new ManifestResolver(manifestUrl, name).resolve() const installationResult = await new LanguageServerResolver( manifest, - lspWorkspaceName, + name, new Range(supportedLspServerVersions) ).resolve() @@ -31,7 +31,7 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - await cleanUpLSPDownloads(manifest.versions, path.dirname(installationResult.assetDirectory)) + await cleanUpLSPDownloads(manifest.versions, path.basename(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: { From 88db689a975256b7c40a189c4032627efe4cd00b Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 13:30:35 -0500 Subject: [PATCH 16/18] refactor: move cleanup work to more general location --- packages/amazonq/src/lsp/lspInstaller.ts | 2 +- packages/core/src/amazonq/index.ts | 1 - packages/core/src/amazonq/lsp/workspaceInstaller.ts | 2 +- packages/core/src/shared/index.ts | 1 + .../{amazonq/lsp/util.ts => shared/lsp/utils/cleanup.ts} | 8 ++++---- packages/core/src/test/amazonq/lsp/util.test.ts | 3 +-- 6 files changed, 8 insertions(+), 9 deletions(-) rename packages/core/src/{amazonq/lsp/util.ts => shared/lsp/utils/cleanup.ts} (85%) diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index 1218b00751d..2f105527ae5 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -12,9 +12,9 @@ import { fs, LspResolution, getNodeExecutableName, + cleanUpLSPDownloads, } from 'aws-core-vscode/shared' import path from 'path' -import { cleanUpLSPDownloads } from 'aws-core-vscode/amazonq' const manifestURL = 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json' export const supportedLspServerVersions = '^2.3.0' diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 1ad208914ee..c5abbf7658e 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -17,7 +17,6 @@ export { } from './onboardingPage/walkthrough' export { LspController } from './lsp/lspController' export { LspClient } from './lsp/lspClient' -export { cleanUpLSPDownloads } from './lsp/util' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' export { init as cwChatAppInit } from '../codewhispererChat/app' diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index 845d7d1c47c..d6e62d3d552 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -10,7 +10,7 @@ import { LanguageServerResolver } from '../../shared/lsp/lspResolver' import { Range } from 'semver' import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' -import { cleanUpLSPDownloads } from './util' +import { cleanUpLSPDownloads } from '../../shared' const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 7cdf3ad12f0..236badac4be 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -63,6 +63,7 @@ export { TabTypeDataMap } from '../amazonq/webview/ui/tabs/constants' export * from './lsp/manifestResolver' export * from './lsp/lspResolver' export * from './lsp/types' +export * from './lsp/utils/cleanup' export { default as request } from './request' export * from './lsp/utils/platform' export * as processUtils from './utilities/processUtils' diff --git a/packages/core/src/amazonq/lsp/util.ts b/packages/core/src/shared/lsp/utils/cleanup.ts similarity index 85% rename from packages/core/src/amazonq/lsp/util.ts rename to packages/core/src/shared/lsp/utils/cleanup.ts index 75546bd5b1c..16e1cd456f0 100644 --- a/packages/core/src/amazonq/lsp/util.ts +++ b/packages/core/src/shared/lsp/utils/cleanup.ts @@ -4,12 +4,12 @@ */ import path from 'path' -import { LspVersion } from '../../shared' -import { fs } from '../../shared/fs/fs' -import { partition } from '../../shared/utilities/tsUtils' +import { LspVersion } from '../types' +import { fs } from '../../../shared/fs/fs' +import { partition } from '../../../shared/utilities/tsUtils' import { sort } from 'semver' -export async function getDownloadedVersions(installLocation: string) { +async function getDownloadedVersions(installLocation: string) { return (await fs.readdir(installLocation)).map(([f, _], __) => f) } diff --git a/packages/core/src/test/amazonq/lsp/util.test.ts b/packages/core/src/test/amazonq/lsp/util.test.ts index 22ea6bbed52..dfa97bf15db 100644 --- a/packages/core/src/test/amazonq/lsp/util.test.ts +++ b/packages/core/src/test/amazonq/lsp/util.test.ts @@ -4,11 +4,10 @@ */ import { Uri } from 'vscode' -import { fs } from '../../../shared' +import { cleanUpLSPDownloads, fs } from '../../../shared' import { createTestWorkspaceFolder } from '../../testUtil' import path from 'path' import assert from 'assert' -import { cleanUpLSPDownloads } from '../../../amazonq/lsp/util' async function fakeInstallVersion(version: string, installationDir: string): Promise { const versionDir = path.join(installationDir, version) From bace7ca18e8a76021f0ff9eaedef7dde38c4170f Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 16:24:08 -0500 Subject: [PATCH 17/18] refactor: rename cleanup function and move test file --- .../core/src/amazonq/lsp/workspaceInstaller.ts | 4 ++-- packages/core/src/shared/lsp/utils/cleanup.ts | 2 +- .../lsp/util.test.ts => lsp/util/cleanup.test.ts} | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) rename packages/core/src/test/{amazonq/lsp/util.test.ts => lsp/util/cleanup.test.ts} (87%) diff --git a/packages/core/src/amazonq/lsp/workspaceInstaller.ts b/packages/core/src/amazonq/lsp/workspaceInstaller.ts index d6e62d3d552..c4c688d7bc1 100644 --- a/packages/core/src/amazonq/lsp/workspaceInstaller.ts +++ b/packages/core/src/amazonq/lsp/workspaceInstaller.ts @@ -10,7 +10,7 @@ import { LanguageServerResolver } from '../../shared/lsp/lspResolver' import { Range } from 'semver' import { getNodeExecutableName } from '../../shared/lsp/utils/platform' import { fs } from '../../shared/fs/fs' -import { cleanUpLSPDownloads } from '../../shared' +import { cleanLspDownloads } from '../../shared' const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json' // this LSP client in Q extension is only going to work with these LSP server versions @@ -31,7 +31,7 @@ export class WorkspaceLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, nodeName) await fs.chmod(nodePath, 0o755) - await cleanUpLSPDownloads(manifest.versions, path.basename(installationResult.assetDirectory)) + await cleanLspDownloads(manifest.versions, path.basename(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: { diff --git a/packages/core/src/shared/lsp/utils/cleanup.ts b/packages/core/src/shared/lsp/utils/cleanup.ts index 16e1cd456f0..874f56e46ff 100644 --- a/packages/core/src/shared/lsp/utils/cleanup.ts +++ b/packages/core/src/shared/lsp/utils/cleanup.ts @@ -22,7 +22,7 @@ function isDelisted(manifestVersions: LspVersion[], targetVersion: string): bool * @param manifest * @param downloadDirectory */ -export async function cleanUpLSPDownloads(manifestVersions: LspVersion[], downloadDirectory: string): Promise { +export async function cleanLspDownloads(manifestVersions: LspVersion[], downloadDirectory: string): Promise { const downloadedVersions = await getDownloadedVersions(downloadDirectory) const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) => isDelisted(manifestVersions, v) diff --git a/packages/core/src/test/amazonq/lsp/util.test.ts b/packages/core/src/test/lsp/util/cleanup.test.ts similarity index 87% rename from packages/core/src/test/amazonq/lsp/util.test.ts rename to packages/core/src/test/lsp/util/cleanup.test.ts index dfa97bf15db..0ac0bca5096 100644 --- a/packages/core/src/test/amazonq/lsp/util.test.ts +++ b/packages/core/src/test/lsp/util/cleanup.test.ts @@ -4,7 +4,7 @@ */ import { Uri } from 'vscode' -import { cleanUpLSPDownloads, fs } from '../../../shared' +import { cleanLspDownloads, fs } from '../../../shared' import { createTestWorkspaceFolder } from '../../testUtil' import path from 'path' import assert from 'assert' @@ -21,7 +21,7 @@ async function fakeInstallVersions(versions: string[], installationDir: string): } } -describe('cleanUpLSPDownloads', function () { +describe('cleanLSPDownloads', function () { let installationDir: Uri before(async function () { @@ -41,7 +41,7 @@ describe('cleanUpLSPDownloads', function () { it('keeps two newest versions', async function () { await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - await cleanUpLSPDownloads([], installationDir.fsPath) + await cleanLspDownloads([], installationDir.fsPath) const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) assert.strictEqual(result.length, 2) @@ -51,7 +51,7 @@ describe('cleanUpLSPDownloads', function () { it('deletes delisted versions', async function () { await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - await cleanUpLSPDownloads([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) + await cleanLspDownloads([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath) const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) assert.strictEqual(result.length, 2) @@ -61,7 +61,7 @@ describe('cleanUpLSPDownloads', function () { it('handles case where less than 2 versions are not delisted', async function () { await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath) - await cleanUpLSPDownloads( + await cleanLspDownloads( [ { serverVersion: '1.1.1', isDelisted: true, targets: [] }, { serverVersion: '2.1.1', isDelisted: true, targets: [] }, @@ -77,7 +77,7 @@ describe('cleanUpLSPDownloads', function () { it('handles case where less than 2 versions exist', async function () { await fakeInstallVersions(['1.0.0'], installationDir.fsPath) - await cleanUpLSPDownloads([], installationDir.fsPath) + await cleanLspDownloads([], installationDir.fsPath) const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) assert.strictEqual(result.length, 1) @@ -85,7 +85,7 @@ describe('cleanUpLSPDownloads', function () { it('does not install delisted version when no other option exists', async function () { await fakeInstallVersions(['1.0.0'], installationDir.fsPath) - await cleanUpLSPDownloads([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath) + await cleanLspDownloads([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath) const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename) assert.strictEqual(result.length, 0) From da207820896916ce8adabd36cef39cf381acc91a Mon Sep 17 00:00:00 2001 From: hkobew Date: Fri, 24 Jan 2025 17:56:44 -0500 Subject: [PATCH 18/18] fix: migrate old name --- packages/amazonq/src/lsp/lspInstaller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index 2f105527ae5..72d0746cdcf 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -12,7 +12,7 @@ import { fs, LspResolution, getNodeExecutableName, - cleanUpLSPDownloads, + cleanLspDownloads, } from 'aws-core-vscode/shared' import path from 'path' @@ -47,7 +47,7 @@ export class AmazonQLSPResolver implements LspResolver { const nodePath = path.join(installationResult.assetDirectory, `servers/${getNodeExecutableName()}`) await fs.chmod(nodePath, 0o755) - await cleanUpLSPDownloads(manifest.versions, path.dirname(installationResult.assetDirectory)) + await cleanLspDownloads(manifest.versions, path.dirname(installationResult.assetDirectory)) return { ...installationResult, resourcePaths: {