diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04dbdd11a26..9992cd16dcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,7 +382,7 @@ If you need to report an issue attach these to give the most detailed informatio - ![](./docs/images/logsView.png) 2. Click the gear icon on the bottom right and select `Debug` - ![](./docs/images/logsSetDebug.png) -3. Click the gear icon again and select `Set As Default`. This will ensure we stay in `Debug` until explicitly changed +3. Click the gear icon again and select `Set As Default`. This will ensure we stay in `Debug` until explicitly changed. - ![](./docs/images/logsSetDefault.png) 4. Open the Command Palette again and select `Reload Window`. 5. Now you should see additional `[debug]` prefixed logs in the output. diff --git a/buildspec/release/50githubrelease.yml b/buildspec/release/50githubrelease.yml index c994b4111a6..df542cbee14 100644 --- a/buildspec/release/50githubrelease.yml +++ b/buildspec/release/50githubrelease.yml @@ -36,10 +36,13 @@ phases: - echo "posting $VERSION with sha384 hash $HASH to GitHub" - PKG_DISPLAY_NAME=$(grep -m 1 displayName packages/${TARGET_EXTENSION}/package.json | grep -o '[a-zA-z][^\"]\+' | tail -n1) - RELEASE_MESSAGE="${PKG_DISPLAY_NAME} for VS Code $VERSION" + # Only set amazonq as "latest" release. This ensures https://api.github.com/repos/aws/aws-toolkit-vscode/releases/latest + # consistently points to the amazonq artifact, instead of being "random". + - LATEST="$([ "$TARGET_EXTENSION" = amazonq ] && echo '--latest' || echo '--latest=false' )" - | if [ "$STAGE" = "prod" ]; then # note: the tag arg passed here should match what is in 10changeversion.yml - gh release create --repo $REPO --title "$PKG_DISPLAY_NAME $VERSION" --notes "$RELEASE_MESSAGE" -- "${TARGET_EXTENSION}/v${VERSION}" "$UPLOAD_TARGET" "$HASH_UPLOAD_TARGET" + gh release create "$LATEST" --repo $REPO --title "$PKG_DISPLAY_NAME $VERSION" --notes "$RELEASE_MESSAGE" -- "${TARGET_EXTENSION}/v${VERSION}" "$UPLOAD_TARGET" "$HASH_UPLOAD_TARGET" else echo "SKIPPED (stage=${STAGE}): 'gh release create --repo $REPO'" fi diff --git a/package-lock.json b/package-lock.json index aa538ee968b..dd771e45ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.322", + "@aws-toolkits/telemetry": "^1.0.323", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -10879,9 +10879,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.322", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.322.tgz", - "integrity": "sha512-KtLabV3ycRH31EAZ0xoWrdpIBG3ym8CQAqgkHd9DSefndbepPRa07atfXw73Ok9J5aA81VHCFpx1dwrLg39EcQ==", + "version": "1.0.323", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.323.tgz", + "integrity": "sha512-Wc6HE+l5iJm/3TYx8Y8pU99ffmq78FgDDVMKjYG9Mfr4cXO4PEkB6XOkiVwGYnrNOGWqyYNlnkBFJ32WJRfkKg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11098,9 +11098,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.30.3", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.30.3.tgz", - "integrity": "sha512-Xy22dzCaFUqpdSHMpLa8Dsq98DiAUq49dm7Iu8Yj2YZXSCyfKQiYMJOfwU8IoqeNcEney5JRMJpf+/RysWugbA==", + "version": "4.34.1", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.34.1.tgz", + "integrity": "sha512-CO65lwedf6Iw3a3ULOl+9EHafIekiPlP+n8QciN9a3POfsRamHl0kpBGaGBzBRgsQ/h5R0FvFG/gAuWoiK/YIA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { @@ -25291,7 +25291,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.70.0-SNAPSHOT", + "version": "1.74.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -25334,7 +25334,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.30.3", + "@aws/mynah-ui": "^4.34.1", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", @@ -27005,7 +27005,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.64.0-SNAPSHOT", + "version": "3.65.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index 525655b8c35..751144b9f47 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.322", + "@aws-toolkits/telemetry": "^1.0.323", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", diff --git a/packages/amazonq/.changes/1.70.0.json b/packages/amazonq/.changes/1.70.0.json new file mode 100644 index 00000000000..841e8107430 --- /dev/null +++ b/packages/amazonq/.changes/1.70.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-05-28", + "version": "1.70.0", + "entries": [ + { + "type": "Removal", + "description": "Disable local workspace LSP" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.71.0.json b/packages/amazonq/.changes/1.71.0.json new file mode 100644 index 00000000000..be5cc5a2013 --- /dev/null +++ b/packages/amazonq/.changes/1.71.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-06-04", + "version": "1.71.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.72.0.json b/packages/amazonq/.changes/1.72.0.json new file mode 100644 index 00000000000..10b0b374c3a --- /dev/null +++ b/packages/amazonq/.changes/1.72.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-06-11", + "version": "1.72.0", + "entries": [ + { + "type": "Feature", + "description": "Launch LSP with bundled artifacts as fallback" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.73.0.json b/packages/amazonq/.changes/1.73.0.json new file mode 100644 index 00000000000..25cda6dcf03 --- /dev/null +++ b/packages/amazonq/.changes/1.73.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-06-11", + "version": "1.73.0", + "entries": [ + { + "type": "Feature", + "description": "Add MCP Server Support" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json b/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json deleted file mode 100644 index 4c95991dbb6..00000000000 --- a/packages/amazonq/.changes/next-release/Removal-951c2b6a-c6ce-45df-95d0-381ca51b935f.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Removal", - "description": "Disable local workspace LSP" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 30d7ee956c1..31910bedf7f 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.73.0 2025-06-11 + +- **Feature** Add MCP Server Support + +## 1.72.0 2025-06-11 + +- **Feature** Launch LSP with bundled artifacts as fallback + +## 1.71.0 2025-06-04 + +- Miscellaneous non-user-facing changes + +## 1.70.0 2025-05-28 + +- **Removal** Disable local workspace LSP + ## 1.69.0 2025-05-22 - **Bug Fix** /transform: avoid prompting user for target JDK path unnecessarily diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 7a3aab1f8d6..2cb805333c1 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.70.0-SNAPSHOT", + "version": "1.74.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -213,6 +213,12 @@ "items": { "type": "string" } + }, + "amazonQ.proxy.certificateAuthority": { + "type": "string", + "markdownDescription": "%AWS.configuration.description.amazonq.proxy.certificateAuthority%", + "default": null, + "scope": "application" } } }, @@ -382,16 +388,21 @@ "when": "view == aws.amazonq.AmazonQChatView", "group": "0_topAmazonQ@1" }, - { - "command": "aws.amazonq.learnMore", - "when": "view =~ /^aws\\.amazonq/", - "group": "1_amazonQ@1" - }, { "command": "aws.amazonq.selectRegionProfile", "when": "view == aws.amazonq.AmazonQChatView && aws.amazonq.connectedSsoIdc == true", "group": "1_amazonQ@1" }, + { + "command": "aws.amazonq.manageSubscription", + "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected", + "group": "1_amazonQ@2" + }, + { + "command": "aws.amazonq.learnMore", + "when": "view =~ /^aws\\.amazonq/", + "group": "1_amazonQ@3" + }, { "command": "aws.amazonq.signout", "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio", @@ -673,6 +684,13 @@ "category": "%AWS.amazonq.title%", "icon": "$(question)" }, + { + "command": "aws.amazonq.manageSubscription", + "title": "%AWS.command.manageSubscription%", + "category": "%AWS.amazonq.title%", + "icon": "$(gear)", + "enablement": "aws.codewhisperer.connected && !aws.amazonq.connectedSsoIdc" + }, { "command": "aws.amazonq.signout", "title": "%AWS.command.codewhisperer.signout%", diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index af48bc65e05..659115d4256 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode' import { ExtensionContext } from 'vscode' import { telemetry } from 'aws-core-vscode/telemetry' import { AuthUtil } from 'aws-core-vscode/codewhisperer' -import { Commands, placeholder } from 'aws-core-vscode/shared' +import { Commands, getLogger, placeholder } from 'aws-core-vscode/shared' import * as amazonq from 'aws-core-vscode/amazonq' export async function activate(context: ExtensionContext) { @@ -67,7 +67,9 @@ async function setupAuthNotification() { const selection = await vscode.window.showWarningMessage('Start using Amazon Q', buttonAction) if (selection === buttonAction) { - void amazonq.focusAmazonQPanel.execute(placeholder, source) + amazonq.focusAmazonQPanel.execute(placeholder, source).catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) } } } diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 45641b37440..1a9d3c5facc 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -34,6 +34,7 @@ import { Experiments, isSageMaker, isAmazonLinux2, + ProxyUtil, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' @@ -119,6 +120,10 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is const extContext = { extensionContext: context, } + + // Configure proxy settings early + ProxyUtil.configureProxyForLanguageServer() + // This contains every lsp agnostic things (auth, security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) if ( @@ -166,7 +171,9 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // Give time for the extension to finish initializing. globals.clock.setTimeout(async () => { CommonAuthWebview.authSource = ExtStartUpSources.firstStartUp - void focusAmazonQPanel.execute(placeholder, ExtStartUpSources.firstStartUp) + focusAmazonQPanel.execute(placeholder, ExtStartUpSources.firstStartUp).catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) }, 1000) } diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index 84bae8a01a6..e2c1b6899d9 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -5,8 +5,8 @@ import vscode from 'vscode' import { startLanguageServer } from './client' -import { AmazonQLspInstaller } from './lspInstaller' -import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared' +import { AmazonQLspInstaller, getBundledResourcePaths } from './lspInstaller' +import { lspSetupStage, ToolkitError, messages, getLogger } from 'aws-core-vscode/shared' export async function activate(ctx: vscode.ExtensionContext): Promise { try { @@ -16,6 +16,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }) } catch (err) { const e = err as ToolkitError - void messages.showViewLogsMessage(`Failed to launch Amazon Q language server: ${e.message}`) + getLogger('amazonqLsp').warn(`Failed to start downloaded LSP, falling back to bundled LSP: ${e.message}`) + try { + await lspSetupStage('all', async () => { + await lspSetupStage('launch', async () => await startLanguageServer(ctx, getBundledResourcePaths(ctx))) + }) + } catch (error) { + void messages.showViewLogsMessage( + `Failed to launch Amazon Q language server: ${(error as ToolkitError).message}` + ) + } } } diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts index d81f464d6a3..0bfee98f2e2 100644 --- a/packages/amazonq/src/lsp/auth.ts +++ b/packages/amazonq/src/lsp/auth.ts @@ -96,6 +96,8 @@ export class AmazonQLspAuth { token, }) + // "aws/credentials/token/update" + // https://github.com/aws/language-servers/blob/44d81f0b5754747d77bda60b40cc70950413a737/core/aws-lsp-core/src/credentials/credentialsProvider.ts#L27 await this.client.sendRequest(bearerCredentialsUpdateRequestType.method, request) this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index f8e3ee16251..e10a7d2d438 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -6,7 +6,7 @@ import { window } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' -import { registerCommands } from './commands' +import { focusAmazonQPanel, registerCommands } from './commands' import { registerLanguageServerEventListener, registerMessageListeners } from './messages' import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/shared' import { activate as registerLegacyChatListeners } from '../../app/chat/activation' @@ -73,6 +73,18 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu customization: undefinedIfEmpty(getSelectedCustomization().arn), }) }), + Commands.register('aws.amazonq.manageSubscription', () => { + focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + + languageClient + .sendRequest('workspace/executeCommand', { + command: 'aws/chat/manageSubscription', + // arguments: [], + }) + .catch((e) => { + getLogger('amazonqLsp').error('failed request: aws/chat/manageSubscription: %O', e) + }) + }), globals.logOutputChannel.onDidChangeLogLevel((logLevel) => { getLogger('amazonqLsp').info(`Local log level changed to ${logLevel}, notifying LSP`) void pushConfigUpdate(languageClient, { diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index 74c63592a4f..115118a4ad2 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -125,7 +125,7 @@ function registerGenericCommand(commandName: string, genericCommand: string, pro * * Instead, we just create our own as a temporary solution */ -async function focusAmazonQPanel() { +export async function focusAmazonQPanel() { await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus') } diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 89d221e9442..bbac828e3df 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -53,6 +53,8 @@ import { CancellationTokenSource, chatUpdateNotificationType, ChatUpdateParams, + chatOptionsUpdateType, + ChatOptionsUpdateParams, } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' @@ -70,6 +72,7 @@ import { } from 'aws-core-vscode/amazonq' import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' +import { focusAmazonQPanel } from './commands' export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.info( @@ -332,7 +335,7 @@ export function registerMessageListeners( ) if (!buttonResult.success) { languageClient.error( - `[VSCode Client] Failed to execute action associated with button with reason: ${buttonResult.failureReason}` + `[VSCode Client] Failed to execute button action: ${buttonResult.failureReason}` ) } break @@ -431,8 +434,25 @@ export function registerMessageListeners( languageClient.onRequest( ShowDocumentRequest.method, async (params: ShowDocumentParams): Promise> => { + focusAmazonQPanel().catch((e) => languageClient.error(`[VSCode Client] focusAmazonQPanel() failed`)) + try { const uri = vscode.Uri.parse(params.uri) + + if (params.external) { + // Note: Not using openUrl() because we probably don't want telemetry for these URLs. + // Also it doesn't yet support the required HACK below. + + // HACK: workaround vscode bug: https://github.com/microsoft/vscode/issues/85930 + vscode.env.openExternal(params.uri as any).then(undefined, (e) => { + // TODO: getLogger('?').error('failed vscode.env.openExternal: %O', e) + vscode.env.openExternal(uri).then(undefined, (e) => { + // TODO: getLogger('?').error('failed vscode.env.openExternal: %O', e) + }) + }) + return params + } + const doc = await vscode.workspace.openTextDocument(uri) await vscode.window.showTextDocument(doc, { preview: false }) return params @@ -486,6 +506,13 @@ export function registerMessageListeners( params: params, }) }) + + languageClient.onNotification(chatOptionsUpdateType.method, (params: ChatOptionsUpdateParams) => { + void provider.webview?.postMessage({ + command: chatOptionsUpdateType.method, + params: params, + }) + }) } function isServerEvent(command: string) { diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index 1a513f1df3f..bb190b5eb67 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -19,6 +19,7 @@ import { AmazonQPromptSettings, LanguageServerResolver, amazonqMark, + getLogger, } from 'aws-core-vscode/shared' import { AuthUtil, RegionProfile } from 'aws-core-vscode/codewhisperer' import { featureConfig } from 'aws-core-vscode/amazonq' @@ -44,9 +45,12 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { ) { const lspDir = Uri.file(LanguageServerResolver.defaultDir()) const dist = Uri.joinPath(globals.context.extensionUri, 'dist') - - const resourcesRoots = [lspDir, dist] - + const bundledResources = Uri.joinPath(globals.context.extensionUri, 'resources/language-server') + let resourcesRoots = [lspDir, dist] + if (this.mynahUIPath?.startsWith(globals.context.extensionUri.fsPath)) { + getLogger('amazonqLsp').info(`Using bundled webview resources ${bundledResources.fsPath}`) + resourcesRoots = [bundledResources, dist] + } /** * if the mynah chat client is defined, then make sure to add it to the resource roots, otherwise * it will 401 when trying to load diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 549b0ac7dad..01dac742902 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -123,6 +123,7 @@ export async function startLanguageServer( awsClientCapabilities: { q: { developerProfiles: true, + mcp: true, }, window: { notifications: true, diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index 84d5ee8961b..9ac19601fe7 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import vscode from 'vscode' import { fs, getNodeExecutableName, getRgExecutableName, BaseLspInstaller, ResourcePaths } from 'aws-core-vscode/shared' import path from 'path' import { ExtendedAmazonQLSPConfig, getAmazonQLspConfig } from './config' @@ -54,3 +55,13 @@ export class AmazonQLspInstaller extends BaseLspInstaller.BaseLspInstaller< protected override downloadMessageOverride: string | undefined = 'Updating Amazon Q plugin' } + +export function getBundledResourcePaths(ctx: vscode.ExtensionContext): AmazonQResourcePaths { + const assetDirectory = vscode.Uri.joinPath(ctx.extensionUri, 'resources', 'language-server').fsPath + return { + lsp: path.join(assetDirectory, 'servers', 'aws-lsp-codewhisperer.js'), + node: process.execPath, + ripGrep: '', + ui: path.join(assetDirectory, 'clients', 'amazonq-ui.js'), + } +} diff --git a/packages/core/package.json b/packages/core/package.json index f35369cc5b9..67e20d5feb1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -526,7 +526,7 @@ "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.30.3", + "@aws/mynah-ui": "^4.34.1", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 9922ec6fcd8..aa1ac167917 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -98,7 +98,7 @@ "AWS.configuration.description.amazonq.workspaceIndexIgnoreFilePatterns": "File patterns to ignore when indexing your workspace files", "AWS.configuration.description.amazonq.workspaceIndexCacheDirPath": "The path to the directory that contains the cache of the index of your workspace files", "AWS.configuration.description.amazonq.ignoredSecurityIssues": "Specifies a list of code issue identifiers that Amazon Q should ignore when reviewing your workspace. Each item in the array should be a unique string identifier for a specific code issue. This allows you to suppress notifications for known issues that you've assessed and determined to be false positives or not applicable to your project. Use this setting with caution, as it may cause you to miss important security alerts.", - "AWS.command.apig.copyUrl": "Copy URL", + "AWS.configuration.description.amazonq.proxy.certificateAuthority": "Path to a Certificate Authority (PEM file) for SSL/TLS verification when using a proxy.", "AWS.command.apig.invokeRemoteRestApi": "Invoke in the cloud", "AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon", "AWS.appBuilder.explorerTitle": "Application Builder", @@ -137,6 +137,7 @@ "AWS.command.codecatalyst.login": "Connect to CodeCatalyst", "AWS.command.codecatalyst.logout": "Sign out of CodeCatalyst", "AWS.command.codecatalyst.signout": "Sign Out", + "AWS.command.manageSubscription": "Manage Q Developer Pro Subscription", "AWS.command.amazonq.explainCode": "Explain", "AWS.command.amazonq.refactorCode": "Refactor", "AWS.command.amazonq.fixCode": "Fix", diff --git a/packages/core/src/amazonq/auth/controller.ts b/packages/core/src/amazonq/auth/controller.ts index 9cc09ef17cb..5b9772d686a 100644 --- a/packages/core/src/amazonq/auth/controller.ts +++ b/packages/core/src/amazonq/auth/controller.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { getLogger } from '../../shared/logger/logger' import { reconnect } from '../../codewhisperer/commands/basicCommands' import { amazonQChatSource } from '../../codewhisperer/commands/types' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' @@ -27,7 +28,9 @@ export class AuthController { } private handleFullAuth() { - void focusAmazonQPanel.execute(placeholder, 'amazonQChat') + focusAmazonQPanel.execute(placeholder, 'amazonQChat').catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) } private handleReAuth() { diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index ed7d6a1d1fe..8578c72377a 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -63,7 +63,8 @@ To learn more, visit the [User Guide](${userGuideURL}).`, gumby: { title: 'Q - Code Transformation', placeholder: 'Open a new tab to chat with Q', - welcome: 'Welcome to Code Transformation!', + welcome: + 'Welcome to Code Transformation! You can also run transformations from the command line. To install the tool, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/run-CLI-transformations.html).', }, review: { title: 'Q - Review', diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 9d15271aa1e..5265cb5b888 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -739,7 +739,7 @@ dependencyManagement: - identifier: "com.example:library1" targetVersion: "2.1.0" versionProperty: "library1.version" # Optional - originType: "FIRST_PARTY" # or "THIRD_PARTY" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY" - identifier: "com.example:library2" targetVersion: "3.0.0" originType: "THIRD_PARTY" diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 02a0067be45..c180d603c67 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -24,3 +24,4 @@ export { Auth } from './auth' export { CredentialsStore } from './credentials/store' export { LoginManager } from './deprecated/loginManager' export * as AuthUtils from './utils' +export * as credentialsValidation from './credentials/validation' diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index 7fe6078a1d7..a24c6ade704 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -405,7 +405,9 @@ export const notifyNewCustomizationsCmd = Commands.declare( function focusQAfterDelay() { // this command won't work without a small delay after install globals.clock.setTimeout(() => { - void focusAmazonQPanel.execute(placeholder, 'startDelay') + focusAmazonQPanel.execute(placeholder, 'startDelay').catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) }, 1000) } @@ -597,7 +599,10 @@ export const signoutCodeWhisperer = Commands.declare( (auth: AuthUtil) => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { await auth.secondaryAuth.deleteConnection() SecurityIssueTreeViewProvider.instance.refresh() - return focusAmazonQPanel.execute(placeholder, source) + return focusAmazonQPanel.execute(placeholder, source).catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + return undefined + }) } ) diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 5e8256b7f77..91e9ad00ab9 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -677,7 +677,7 @@ export async function postTransformationJob() { let chatMessage = transformByQState.getJobFailureErrorChatMessage() if (transformByQState.isSucceeded()) { - chatMessage = CodeWhispererConstants.jobCompletedChatMessage + chatMessage = CodeWhispererConstants.jobCompletedChatMessage(transformByQState.getTargetJDKVersion() ?? '') } else if (transformByQState.isPartiallySucceeded()) { chatMessage = CodeWhispererConstants.jobPartiallyCompletedChatMessage } @@ -708,9 +708,12 @@ export async function postTransformationJob() { } if (transformByQState.isSucceeded()) { - void vscode.window.showInformationMessage(CodeWhispererConstants.jobCompletedNotification, { - title: localizedText.ok, - }) + void vscode.window.showInformationMessage( + CodeWhispererConstants.jobCompletedNotification(transformByQState.getTargetJDKVersion() ?? ''), + { + title: localizedText.ok, + } + ) } else if (transformByQState.isPartiallySucceeded()) { void vscode.window .showInformationMessage( diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 289a89828c3..e5cd9525ddb 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -655,9 +655,11 @@ export const enterJavaHomePlaceholder = 'Enter the path to your Java installatio export const openNewTabPlaceholder = 'Open a new tab to chat with Q' -export const jobCompletedChatMessage = `I completed your transformation. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes I'm proposing.` +export const jobCompletedChatMessage = (version: string) => + `I completed your transformation. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes I'm proposing. If you want to upgrade additional libraries and other dependencies, run /transform with the transformed code and specify ${version} as the source and target version.` -export const jobCompletedNotification = `Amazon Q transformed your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes.` +export const jobCompletedNotification = (version: string) => + `Amazon Q transformed your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes. If you want to upgrade additional libraries and other dependencies, run /transform with the transformed code and specify ${version} as the source and target version.` export const jobPartiallyCompletedChatMessage = `I transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation.` diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 128d34757fc..d77c52254bc 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -676,8 +676,8 @@ export class ZipManifest { version: string = '1.0' hilCapabilities: string[] = ['HIL_1pDependency_VersionUpgrade'] // TO-DO: add 'CLIENT_SIDE_BUILD' here when releasing - // TO-DO: add something like AGENTIC_PLAN_V1 here when BE allowlists everyone - transformCapabilities: string[] = ['EXPLAINABILITY_V1'] + transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2'] + noInteractiveMode: boolean = true customBuildCommand: string = 'clean test' requestedConversions?: { sqlConversion?: { diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index c3e46bdc78e..28ed3952494 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -28,6 +28,7 @@ import { AuthUtil } from '../util/authUtil' import { submitFeedback } from '../../feedback/vue/submitFeedback' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { isWeb } from '../../shared/extensionGlobals' +import { getLogger } from '../../shared/logger/logger' export function createAutoSuggestions(running: boolean): DataQuickPickItem<'autoSuggestions'> { const labelResume = localize('AWS.codewhisperer.resumeCodeWhispererNode.label', 'Resume Auto-Suggestions') @@ -175,6 +176,18 @@ export function createGettingStarted(): DataQuickPickItem<'gettingStarted'> { } as DataQuickPickItem<'gettingStarted'> } +export function createManageSubscription(): DataQuickPickItem<'manageSubscription'> { + const label = localize('AWS.command.manageSubscription', 'Manage Q Developer Pro Subscription') + // const kind = AuthUtil.instance.isBuilderIdInUse() ? 'AWS Builder ID' : 'IAM Identity Center' + + return { + data: 'manageSubscription', + label: label, + iconPath: getIcon('vscode-link-external'), + onClick: () => Commands.tryExecute('aws.amazonq.manageSubscription'), + } as DataQuickPickItem<'manageSubscription'> +} + export function createSignout(): DataQuickPickItem<'signout'> { const label = localize('AWS.codewhisperer.signoutNode.label', 'Sign Out') const icon = getIcon('vscode-export') @@ -238,7 +251,10 @@ export function switchToAmazonQNode(): DataQuickPickItem<'openChatPanel'> { data: 'openChatPanel', label: 'Open Chat Panel', iconPath: getIcon('vscode-comment'), - onClick: () => focusAmazonQPanel.execute(placeholder, 'codewhispererQuickPick'), + onClick: () => + focusAmazonQPanel.execute(placeholder, 'codewhispererQuickPick').catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }), } } @@ -247,7 +263,9 @@ export function createSignIn(): DataQuickPickItem<'signIn'> { const icon = getIcon('vscode-account') let onClick = () => { - void focusAmazonQPanel.execute(placeholder, 'codewhispererQuickPick') + focusAmazonQPanel.execute(placeholder, 'codewhispererQuickPick').catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) } if (isWeb()) { // TODO: nkomonen, call a Command instead diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 2ad14a81df0..46f47e35a2c 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -11,6 +11,7 @@ import { createSelectCustomization, createReconnect, createGettingStarted, + createManageSubscription, createSignout, createSeparator, createSettingsNode, @@ -106,7 +107,7 @@ export function getQuickPickItems(): DataQuickPickItem[] { createSettingsNode(), ...(isUsingEnterpriseSso && regionProfile ? [createSelectRegionProfileNode(regionProfile)] : []), ...(AuthUtil.instance.isConnected() && !hasVendedIamCredentials() && !hasVendedCredentialsFromMetadata() - ? [createSignout()] + ? [...(AuthUtil.instance.isBuilderIdInUse() ? [createManageSubscription()] : []), createSignout()] : []), ] diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 10acbe16424..e5177e7b578 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -267,7 +267,9 @@ export class AuthUtil { } catch (err) { if (err instanceof ProfileNotFoundError) { // Expected that connection would be deleted by conn.getToken() - void focusAmazonQPanel.execute(placeholder, 'profileNotFoundSignout') + focusAmazonQPanel.execute(placeholder, 'profileNotFoundSignout').catch((e) => { + getLogger().error('focusAmazonQPanel failed: %s', e) + }) } throw err } diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index ddcd1d91c28..312aa18029b 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -108,8 +108,8 @@ @toggle="toggleItemSelection" :isSelected="selectedLoginOption === LoginOption.BUILDER_ID" :itemId="LoginOption.BUILDER_ID" - :itemText="'with Builder ID, a personal profile from AWS'" - :itemTitle="'Use for Free'" + :itemText="'Free to start with a Builder ID.'" + :itemTitle="'Personal account'" :itemType="LoginOption.BUILDER_ID" class="selectable-item bottomMargin" > @@ -118,8 +118,8 @@ @toggle="toggleItemSelection" :isSelected="selectedLoginOption === LoginOption.ENTERPRISE_SSO" :itemId="LoginOption.ENTERPRISE_SSO" - :itemText="''" - :itemTitle="'Use with Pro license'" + :itemText="'Best for individual teams or organizations.'" + :itemTitle="'Company account'" :itemType="LoginOption.ENTERPRISE_SSO" class="selectable-item bottomMargin" > diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index f4c78e2093c..799ffb1b35c 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -39,6 +39,7 @@ export { CodewhispererUserDecision, CodewhispererSecurityScan, } from './telemetry/telemetry.gen' +export { ProxyUtil } from './utilities/proxyUtil' export { randomUUID } from './crypto' export * from './environmentVariables' export * from './vscode/setContext' diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index b398ff93162..eb2602c30b9 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -21,6 +21,7 @@ export type LogTopic = | 'nextEditPrediction' | 'resourceCache' | 'telemetry' + | 'proxyUtil' class ErrorLog { constructor( diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 637c5b1b12e..836b68444f2 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -36,7 +36,8 @@ export const amazonqSettings = { "amazonQ.workspaceIndexMaxFileSize": {}, "amazonQ.workspaceIndexCacheDirPath": {}, "amazonQ.workspaceIndexIgnoreFilePatterns": {}, - "amazonQ.ignoredSecurityIssues": {} + "amazonQ.ignoredSecurityIssues": {}, + "amazonQ.proxy.certificateAuthority": {} } export default amazonqSettings diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts new file mode 100644 index 00000000000..4e0e5c940b5 --- /dev/null +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -0,0 +1,80 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import vscode from 'vscode' +import { getLogger } from '../logger/logger' + +interface ProxyConfig { + proxyUrl: string | undefined + certificateAuthority: string | undefined +} + +/** + * Utility class for handling proxy configuration + */ +export class ProxyUtil { + private static readonly logger = getLogger('proxyUtil') + + /** + * Sets proxy environment variables based on VS Code settings for use with the Flare Language Server + * + * See documentation here for setting the environement variables which are inherited by Flare LS process: + * https://github.com/aws/language-server-runtimes/blob/main/runtimes/docs/proxy.md + */ + public static configureProxyForLanguageServer(): void { + try { + const proxyConfig = this.getProxyConfiguration() + + this.setProxyEnvironmentVariables(proxyConfig) + } catch (err) { + this.logger.error(`Failed to configure proxy: ${err}`) + } + } + + /** + * Gets proxy configuration from VS Code settings + */ + private static getProxyConfiguration(): ProxyConfig { + const httpConfig = vscode.workspace.getConfiguration('http') + const proxyUrl = httpConfig.get('proxy') + this.logger.debug(`Proxy URL Setting in VSCode Settings: ${proxyUrl}`) + + const amazonQConfig = vscode.workspace.getConfiguration('amazonQ') + const proxySettings = amazonQConfig.get<{ + certificateAuthority?: string + }>('proxy', {}) + + return { + proxyUrl, + certificateAuthority: proxySettings.certificateAuthority, + } + } + + /** + * Sets environment variables based on proxy configuration + */ + private static setProxyEnvironmentVariables(config: ProxyConfig): void { + const proxyUrl = config.proxyUrl + + // Always enable experimental proxy support for better handling of both explicit and transparent proxies + process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = 'true' + // Add OpenSSL certificate store support + process.env.NODE_OPTIONS = '--use-openssl-ca' + + // Set proxy environment variables + if (proxyUrl) { + process.env.HTTPS_PROXY = proxyUrl + process.env.HTTP_PROXY = proxyUrl + this.logger.debug(`Set proxy environment variables: ${proxyUrl}`) + } + + // Set certificate bundle environment variables if configured + if (config.certificateAuthority) { + process.env.NODE_EXTRA_CA_CERTS = config.certificateAuthority + process.env.AWS_CA_BUNDLE = config.certificateAuthority + this.logger.debug(`Set certificate bundle path: ${config.certificateAuthority}`) + } + } +} diff --git a/packages/core/src/shared/utilities/vsCodeUtils.ts b/packages/core/src/shared/utilities/vsCodeUtils.ts index 03229cf104a..57f9e380974 100644 --- a/packages/core/src/shared/utilities/vsCodeUtils.ts +++ b/packages/core/src/shared/utilities/vsCodeUtils.ts @@ -215,8 +215,11 @@ export function reloadWindowPrompt(message: string): void { * if user dismisses the vscode confirmation prompt. */ export async function openUrl(url: vscode.Uri, source?: string): Promise { + // Avoid PII in URL. + const truncatedUrl = `${url.scheme}${url.authority}${url.path}${url.fragment.substring(20)}` + return telemetry.aws_openUrl.run(async (span) => { - span.record({ url: url.toString(), source }) + span.record({ url: truncatedUrl, source }) const didOpen = await vscode.env.openExternal(url) if (!didOpen) { throw new CancellationError('user') diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 01c7c43c947..936e7d84cd6 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -42,6 +42,7 @@ import { createGettingStarted, createGitHubNode, createLearnMore, + createManageSubscription, createOpenReferenceLog, createReconnect, createSecurityScan, @@ -515,6 +516,7 @@ describe('CodeWhisperer-basicCommands', function () { ...genericItems(), createSeparator(), createSettingsNode(), + createManageSubscription(), createSignout(), ]) e.dispose() // skip needing to select an item to continue @@ -537,6 +539,7 @@ describe('CodeWhisperer-basicCommands', function () { switchToAmazonQNode(), ...genericItems(), createSettingsNode(), + createManageSubscription(), createSignout() ) e.dispose() diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index ea2aefce277..369fa1ec67e 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -392,6 +392,8 @@ dependencyManagement: const manifestText = manifestBuffer.toString('utf8') const manifest = JSON.parse(manifestText) assert.strictEqual(manifest.customBuildCommand, CodeWhispererConstants.skipUnitTestsBuildCommand) + assert.strictEqual(manifest.noInteractiveMode, true) + assert.strictEqual(manifest.transformCapabilities.includes('SELECTIVE_TRANSFORMATION_V2'), true) }) }) diff --git a/packages/toolkit/.changes/3.64.0.json b/packages/toolkit/.changes/3.64.0.json new file mode 100644 index 00000000000..c9fd077f42d --- /dev/null +++ b/packages/toolkit/.changes/3.64.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-06-04", + "version": "3.64.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index d2d3f1e9479..2f19a136349 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.64.0 2025-06-04 + +- Miscellaneous non-user-facing changes + ## 3.63.0 2025-05-22 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 309f1e92da2..a433c2693ff 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.64.0-SNAPSHOT", + "version": "3.65.0-SNAPSHOT", "extensionKind": [ "workspace" ], diff --git a/scripts/lspArtifact.ts b/scripts/lspArtifact.ts new file mode 100644 index 00000000000..42b5c59907d --- /dev/null +++ b/scripts/lspArtifact.ts @@ -0,0 +1,183 @@ +import * as https from 'https' +import * as fs from 'fs' +import * as crypto from 'crypto' +import * as path from 'path' +import * as os from 'os' +import * as semver from 'semver' +import AdmZip from 'adm-zip' + +interface ManifestContent { + filename: string + url: string + hashes: string[] + bytes: number +} + +interface ManifestTarget { + platform: string + arch: string + contents: ManifestContent[] +} + +interface ManifestVersion { + serverVersion: string + isDelisted: boolean + targets: ManifestTarget[] +} + +interface Manifest { + versions: ManifestVersion[] +} + +async function verifyFileHash(filePath: string, expectedHash: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha384') + const stream = fs.createReadStream(filePath) + + stream.on('data', (data) => { + hash.update(data) + }) + + stream.on('end', () => { + const fileHash = hash.digest('hex') + // Remove 'sha384:' prefix from expected hash if present + const expectedHashValue = expectedHash.replace('sha384:', '') + resolve(fileHash === expectedHashValue) + }) + + stream.on('error', reject) + }) +} + +async function ensureDirectoryExists(dirPath: string): Promise { + if (!fs.existsSync(dirPath)) { + await fs.promises.mkdir(dirPath, { recursive: true }) + } +} + +export async function downloadLanguageServer(): Promise { + const tempDir = path.join(os.tmpdir(), 'amazonq-download-temp') + const resourcesDir = path.join(__dirname, '../packages/amazonq/resources/language-server') + + // clear previous cached language server + try { + if (fs.existsSync(resourcesDir)) { + fs.rmdirSync(resourcesDir, { recursive: true }) + } + } catch (e) { + throw Error(`Failed to clean up language server ${resourcesDir}`) + } + + await ensureDirectoryExists(tempDir) + await ensureDirectoryExists(resourcesDir) + + return new Promise((resolve, reject) => { + const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json' + + https + .get(manifestUrl, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', async () => { + try { + const manifest: Manifest = JSON.parse(data) + + const latestVersion = manifest.versions + .filter((v) => !v.isDelisted) + .sort((a, b) => semver.compare(b.serverVersion, a.serverVersion))[0] + + if (!latestVersion) { + throw new Error('No valid version found in manifest') + } + + const darwinArm64Target = latestVersion.targets.find( + (t) => t.platform === 'darwin' && t.arch === 'arm64' + ) + + if (!darwinArm64Target) { + throw new Error('No darwin arm64 target found') + } + + for (const content of darwinArm64Target.contents) { + const fileName = content.filename + const fileUrl = content.url + const expectedHash = content.hashes[0] + const tempFilePath = path.join(tempDir, fileName) + const fileFolderName = content.filename.replace('.zip', '') + + console.log(`Downloading ${fileName} from ${fileUrl} ...`) + + await new Promise((downloadResolve, downloadReject) => { + https + .get(fileUrl, (fileRes) => { + const fileStream = fs.createWriteStream(tempFilePath) + fileRes.pipe(fileStream) + + fileStream.on('finish', () => { + fileStream.close() + downloadResolve(void 0) + }) + + fileStream.on('error', (err) => { + fs.unlink(tempFilePath, () => {}) + downloadReject(err) + }) + }) + .on('error', (err) => { + fs.unlink(tempFilePath, () => {}) + downloadReject(err) + }) + }) + + console.log(`Verifying hash for ${fileName}...`) + const isHashValid = await verifyFileHash(tempFilePath, expectedHash) + + if (!isHashValid) { + fs.unlinkSync(tempFilePath) + throw new Error(`Hash verification failed for ${fileName}`) + } + + console.log(`Extracting ${fileName}...`) + const zip = new AdmZip(tempFilePath) + zip.extractAllTo(path.join(resourcesDir, fileFolderName), true) // true for overwrite + + // Clean up temp file + fs.unlinkSync(tempFilePath) + console.log(`Successfully processed ${fileName}`) + } + + // Clean up temp directory + fs.rmdirSync(tempDir) + fs.rmdirSync(path.join(resourcesDir, 'servers', 'indexing'), { recursive: true }) + fs.rmdirSync(path.join(resourcesDir, 'servers', 'ripgrep'), { recursive: true }) + fs.rmSync(path.join(resourcesDir, 'servers', 'node')) + if (!fs.existsSync(path.join(resourcesDir, 'servers', 'aws-lsp-codewhisperer.js'))) { + throw new Error(`Extracting aws-lsp-codewhisperer.js failure`) + } + if (!fs.existsSync(path.join(resourcesDir, 'clients', 'amazonq-ui.js'))) { + throw new Error(`Extracting amazonq-ui.js failure`) + } + console.log('Download and extraction completed successfully') + resolve() + } catch (err) { + // Clean up temp directory on error + if (fs.existsSync(tempDir)) { + fs.rmdirSync(tempDir, { recursive: true }) + } + reject(err) + } + }) + }) + .on('error', (err) => { + // Clean up temp directory on error + if (fs.existsSync(tempDir)) { + fs.rmdirSync(tempDir, { recursive: true }) + } + reject(err) + }) + }) +} diff --git a/scripts/package.ts b/scripts/package.ts index 84622ac12c0..203777e8131 100644 --- a/scripts/package.ts +++ b/scripts/package.ts @@ -20,6 +20,7 @@ import * as child_process from 'child_process' // eslint-disable-line no-restricted-imports import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports import * as path from 'path' +import { downloadLanguageServer } from './lspArtifact' function parseArgs() { // Invoking this script with argument "foo": @@ -105,7 +106,7 @@ function getVersionSuffix(feature: string, debug: boolean): string { return `${debugSuffix}${featureSuffix}${commitSuffix}` } -function main() { +async function main() { const args = parseArgs() // It is expected that this will package from a packages/{subproject} folder. // There is a base config in packages/ @@ -155,6 +156,12 @@ function main() { } nodefs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, undefined, ' ')) + + // add language server bundle + if (packageJson.name === 'amazon-q-vscode') { + await downloadLanguageServer() + } + child_process.execFileSync( 'vsce', [