diff --git a/.changes/next-release/Bug Fix-77137e24-593d-4859-a15f-e029edf843a7.json b/.changes/next-release/Bug Fix-77137e24-593d-4859-a15f-e029edf843a7.json new file mode 100644 index 00000000000..2af1555a690 --- /dev/null +++ b/.changes/next-release/Bug Fix-77137e24-593d-4859-a15f-e029edf843a7.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Resource Explorer: S3 tree view now shows bucket contents correctly, even when restricted to root prefix." +} diff --git a/.eslintrc.js b/.eslintrc.js index c7fc4cd9c13..edfcaa2cb72 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -206,8 +206,15 @@ module.exports = { message: 'Avoid child_process, use ChildProcess from `shared/utilities/processUtils.ts` instead.', }, + { + name: '..', + message: + 'Avoid importing from index.ts files as it can lead to circular dependencies. Import from the module directly instead.', + }, ], }, ], + + 'prettier/prettier': ['error', { endOfLine: 'auto' }], }, } diff --git a/.github/workflows/filterDuplicates.js b/.github/workflows/filterDuplicates.js index 0284ea20654..2bb9d440cb5 100644 --- a/.github/workflows/filterDuplicates.js +++ b/.github/workflows/filterDuplicates.js @@ -4,7 +4,7 @@ * the program exits with an error and logs the filtered report to console. * * Usage: - * node filterDuplicates.js run [path_to_git_diff] [path_to_jscpd_report] + * node filterDuplicates.js run [path_to_git_diff] [path_to_jscpd_report] [commit_hash] [repo_name] * * Tests: * node filterDuplicates.js test @@ -84,9 +84,25 @@ function filterDuplicates(report, changes) { return duplicates } +function formatDuplicates(duplicates, commitHash, repoName) { + const baseUrl = `https://github.com/${repoName}` + return duplicates.map((dupe) => { + return { + first: formUrl(dupe.firstFile, commitHash), + second: formUrl(dupe.secondFile, commitHash), + numberOfLines: dupe.lines, + } + }) + function formUrl(file, commitHash) { + return `${baseUrl}/blob/${commitHash}/${file.name}#L${file.start}-L${file.end}` + } +} + async function run() { const rawDiffPath = process.argv[3] const jscpdReportPath = process.argv[4] + const commitHash = process.argv[5] + const repoName = process.argv[6] const changes = await parseDiff(rawDiffPath) const jscpdReport = JSON.parse(await fs.readFile(jscpdReportPath, 'utf8')) const filteredDuplicates = filterDuplicates(jscpdReport, changes) @@ -94,7 +110,7 @@ async function run() { console.log('%s files changes', changes.size) console.log('%s duplicates found', filteredDuplicates.length) if (filteredDuplicates.length > 0) { - console.log(filteredDuplicates) + console.log(formatDuplicates(filteredDuplicates, commitHash, repoName)) process.exit(1) } } @@ -102,7 +118,6 @@ async function run() { /** * Mini-test Suite */ -console.log(__dirname) const testDiffFile = path.resolve(__dirname, 'test/test_diff.txt') let testCounter = 0 function assertEqual(actual, expected) { diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 04a289eded9..e8c301c18be 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -57,7 +57,7 @@ jobs: - run: npm run testCompile - run: npm run lint - jscpd: + lint-duplicate-code: needs: lint-commits if: ${{ github.event_name == 'pull_request'}} runs-on: ubuntu-latest @@ -101,7 +101,10 @@ jobs: path: ./jscpd-report.json - name: Check for Duplicates - run: node "$GITHUB_WORKSPACE/.github/workflows/filterDuplicates.js" run diff_output.txt jscpd-report.json + env: + COMMIT_HASH: ${{ github.sha}} + REPO_NAME: ${{ github.repository }} + run: node "$GITHUB_WORKSPACE/.github/workflows/filterDuplicates.js" run diff_output.txt jscpd-report.json $COMMIT_HASH $REPO_NAME macos: needs: lint-commits diff --git a/.gitignore b/.gitignore index eeb136eea4a..596af538b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ packages/amazonq/package.nls.json packages/amazonq/resources # Icons -packages/*/resources/icons/cloud9/generated/** packages/*/resources/fonts/aws-toolkit-icons.woff packages/*/resources/css/icons.css diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4dba3042e2..087d6e838ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -633,7 +633,7 @@ If you are contribuing visual assets from other open source repos, the source re ## Using new vscode APIs The minimum required vscode version specified in [package.json](https://github.com/aws/aws-toolkit-vscode/blob/07119655109bb06105a3f53bbcd86b812b32cdbe/package.json#L16) -is decided by the version of vscode running in Cloud9 and other vscode-compatible targets. +is decided by the version of vscode running in other supported vscode-compatible targets (e.g. web). But you can still use the latest vscode APIs, by checking the current running vscode version. For example, to use a vscode 1.64 API: diff --git a/buildspec/release/70checkmarketplace.yml b/buildspec/release/70checkmarketplace.yml new file mode 100644 index 00000000000..670dd2c7508 --- /dev/null +++ b/buildspec/release/70checkmarketplace.yml @@ -0,0 +1,53 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 16 + + commands: + - apt update + - apt install -y wget gpg + - curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg + - install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/ + - sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list' + - apt update + - apt install -y code + + pre_build: + commands: + # Check for implicit env vars passed from the release pipeline. + - test -n "${TARGET_EXTENSION}" + + build: + commands: + - VERSION=$(node -e "console.log(require('./packages/${TARGET_EXTENSION}/package.json').version);") + # get extension name, if in beta, use some hard-coded recent version + - | + if [ "${TARGET_EXTENSION}" = "amazonq" ]; then + extension_name="amazonwebservices.amazon-q-vscode" + [ "$STAGE" != "prod" ] && VERSION="1.43.0" || true + elif [ "${TARGET_EXTENSION}" = "toolkit" ]; then + extension_name="amazonwebservices.aws-toolkit-vscode" + [ "$STAGE" != "prod" ] && VERSION="3.42.0" || true + else + echo checkmarketplace: "Unknown TARGET_EXTENSION: ${TARGET_EXTENSION}" + exit 1 + fi + if [ "$STAGE" != "prod" ]; then + echo "checkmarketplace: Non-production stage detected. Installing hardcoded version '${VERSION}'." + fi + # keep installing the desired extension version until successful. Otherwise fail on codebuild timeout (1 hour). + - | + while true; do + code --uninstall-extension "${extension_name}" --no-sandbox --user-data-dir /tmp/vscode + code --install-extension "${extension_name}@${VERSION}" --no-sandbox --user-data-dir /tmp/vscode || true + cur_version=$(code --list-extensions --show-versions --no-sandbox --user-data-dir /tmp/vscode | grep ${extension_name} | cut -d'@' -f2) + if [ "${cur_version}" = "${VERSION}" ]; then + echo "checkmarketplace: Extension ${extension_name} is updated to version '${cur_version}.'" + break + else + echo "checkmarketplace: Expected extension version '${VERSION}' has not been successfully installed. Retrying..." + fi + sleep 120 # Wait for 2 minutes before retrying + done diff --git a/cloud9-toolkit-install.sh b/cloud9-toolkit-install.sh deleted file mode 100644 index 8b85a56a63a..00000000000 --- a/cloud9-toolkit-install.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/env bash - -# By default, this script gets the latest VSIX from: -# https://github.com/aws/aws-toolkit-vscode/releases/ -# else the first argument must be a URL or file pointing to a toolkit VSIX or -# ZIP (containing a VSIX). -# -# USAGE: -# cloud9-toolkit-install.sh [URL|FILE] -# curl -LO https://raw.githubusercontent.com/aws/aws-toolkit-vscode/master/cloud9-toolkit-install.sh && bash cloud9-toolkit-install.sh -# EXAMPLES: -# cloud9-toolkit-install.sh https://github.com/aws/aws-toolkit-vscode/releases/download/v1.24.0/aws-toolkit-vscode-1.24.0.vsix -# cloud9-toolkit-install.sh toolkit.zip - -set -eu - -# Script currently depends on $HOME so running as root is not supported. -if [ "$(whoami)" = root ]; then - echo "cannot run as root" - exit 1 -fi - -_log() { - echo >&2 "$@" -} - -# Runs whatever is passed. -# -# On failure: -# - prints the command output -# - exits the script -_run() { - local out - if ! out="$("$@" 2>&1)"; then - _log "Command failed (output below): '${*}'" - echo "$out" | sed 's/^/ /' - _log "Command failed (output above): '${*}'" - exit 1 - fi -} - -# Gets the first existing directory, or exits if none are found. -_any_dir() { - for d in "$@"; do - if test -d "$d"; then - echo "$d" - return - fi - done - - _log "error: None of the expected dirs exist:" - for d in "$@"; do - _log " $d" - done - exit 1 -} - -_setglobals() { - # Example: - # https://github.com/aws/aws-toolkit-vscode/releases/tag/v1.24.0 - TOOLKIT_LATEST_RELEASE_URL="$(curl -Ls -o /dev/null -w '%{url_effective}' 'https://github.com/aws/aws-toolkit-vscode/releases/latest')" - # Example: - # 1.24.0 - TOOLKIT_LATEST_VERSION="$(echo "$TOOLKIT_LATEST_RELEASE_URL" | grep -oh '[0-9]\+\.[0-9]\+\.[0-9]\+$')" - # Example: - # https://github.com/aws/aws-toolkit-vscode/releases/download/v1.24.0/aws-toolkit-vscode-1.24.0.vsix - TOOLKIT_LATEST_ARTIFACT_URL="https://github.com/aws/aws-toolkit-vscode/releases/download/v${TOOLKIT_LATEST_VERSION}/aws-toolkit-vscode-${TOOLKIT_LATEST_VERSION}.vsix" - # URL or local filepath pointing to toolkit VSIX or ZIP (containing a VSIX). - TOOLKIT_FILE=${1:-} - TOOLKIT_INSTALL_PARENT="$(_any_dir "/projects/.c9/aws-toolkit-vscode" "$HOME/.c9/dependencies/aws-toolkit-vscode" "$HOME/environment/.c9/extensions" "$HOME/.c9/aws-toolkit-vscode")" - # Hash name is 128 chars long. - TOOLKIT_INSTALL_DIR="$(_any_dir "$(realpath ${TOOLKIT_INSTALL_PARENT}/????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????)" "$(realpath ${TOOLKIT_INSTALL_PARENT}/extension)")" - SCRIPT_WORKDIR="$HOME/toolkit" -} - -_main() { - ( - if test -f "$TOOLKIT_FILE"; then - # Ensure full path (before `cd` below). - TOOLKIT_FILE="$(readlink -f "$TOOLKIT_FILE")" - fi - - echo "Script will DELETE these directories:" - echo " ${TOOLKIT_INSTALL_DIR}" - echo " ${SCRIPT_WORKDIR}" - read -n1 -r -p "ENTER to continue, CTRL-c to cancel" - rm -rf "${TOOLKIT_INSTALL_DIR}.old" - mv "$TOOLKIT_INSTALL_DIR" "${TOOLKIT_INSTALL_DIR}.old" - rm -rf "$SCRIPT_WORKDIR" - - cd "$HOME/" - mkdir -p "$SCRIPT_WORKDIR" - mkdir -p "$TOOLKIT_INSTALL_PARENT" - cd "${SCRIPT_WORKDIR}" - - # Set default URL if no argument was provided. - if test -z "$TOOLKIT_FILE"; then - _log "Latest release:" - _log " URL : $TOOLKIT_LATEST_RELEASE_URL" - _log " version: $TOOLKIT_LATEST_VERSION" - _log " VSIX : $TOOLKIT_LATEST_ARTIFACT_URL" - TOOLKIT_FILE="$TOOLKIT_LATEST_ARTIFACT_URL" - fi - - TOOLKIT_FILE_EXTENSION="${TOOLKIT_FILE: -4}" - if test -f "$TOOLKIT_FILE"; then - _log "Local file (not URL): ${TOOLKIT_FILE}" - if [ "$TOOLKIT_FILE_EXTENSION" = ".zip" ] || [ "$TOOLKIT_FILE_EXTENSION" = ".ZIP" ]; then - _log 'File is a .zip file' - _run unzip -- "$TOOLKIT_FILE" - _run unzip -- *.vsix - else - _log 'File is not .zip file, assuming .vsix' - _run unzip -- "$TOOLKIT_FILE" - fi - else - _log "URL: ${TOOLKIT_FILE}" - _log 'Deleting toolkit.zip' - rm -rf toolkit.zip - _log 'Downloading...' - curl -o toolkit.zip -L "$TOOLKIT_FILE" - if [ "$TOOLKIT_FILE_EXTENSION" = ".zip" ] || [ "$TOOLKIT_FILE_EXTENSION" = ".ZIP" ]; then - _log 'File is a .zip file' - _run unzip -- toolkit.zip - _run unzip -- *.vsix - else - _log 'File is not .zip file, assuming .vsix' - _run unzip -- toolkit.zip - fi - fi - - mv extension "$TOOLKIT_INSTALL_DIR" - _log "Toolkit installed to: $TOOLKIT_INSTALL_DIR" - _log "Refresh Cloud9 to load it" - ) -} - -_setglobals "$@" -_main diff --git a/docs/CODE_GUIDELINES.md b/docs/CODE_GUIDELINES.md index 50471b3d286..90153905b12 100644 --- a/docs/CODE_GUIDELINES.md +++ b/docs/CODE_GUIDELINES.md @@ -81,13 +81,10 @@ that is a net cost. - Telemetry: "active" vs "passive" - Active (`passive:false`) metrics are those intended to appear in DAU count. - Icons: - - Where possible, use IDE-specific standard icons (e.g. standard VSCode - standard icons in VSCode, and Cloud9 standard icons in Cloud9). The typical - (maintainable) way to do this is to use _named_ icons (what VSCode calls + - Where possible, use standard VSCode icons. The typical (maintainable) + way to do this is to use _named_ icons (what VSCode calls [codicons](https://microsoft.github.io/vscode-codicons/)) as opposed to icons shipped with the Toolkit build and referenced by _path_. - - For cases where icons must be statically defined (package.json), if Cloud9 - does not support the VSCode standard icon, use the Cloud9 icon. - Changelog guidelines - Prefer active voice: "You can do X" instead of "X can be done" - Avoid unnecessary use of `lodash` (which we may remove in the future). @@ -240,11 +237,11 @@ _See also [arch_develop.md](./arch_develop.md#exceptions)._ - PREFER: ```ts - things.filter(o => o.isFoo) + things.filter((o) => o.isFoo) ``` - INSTEAD OF: ```ts - things.filter(thing => thing.isFoo) + things.filter((thing) => thing.isFoo) ``` ## User settings diff --git a/docs/arch_runtime.md b/docs/arch_runtime.md index d2c5902c333..e1cbfd01661 100644 --- a/docs/arch_runtime.md +++ b/docs/arch_runtime.md @@ -31,8 +31,6 @@ If you must define a new key (is it _really_ necessary?), follow these guideline These keys are currently set by the core/ package, but many of them may eventually be migrated to toolkit/ or amazonq/ if appropriate. -- `isCloud9`: This is hardcoded by Cloud9 itself, not the Toolkit. - - Cloud9 _does not support setContext_. So this is the only usable key there. - `aws.codecatalyst.connected`: CodeCatalyst connection is active. - `aws.codewhisperer.connected`: CodeWhisperer connection is active. - `aws.codewhisperer.connectionExpired`: CodeWhisperer connection is active, but the connection is expired. diff --git a/docs/icons.md b/docs/icons.md index d5a2144085f..46e252d5dac 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -4,7 +4,6 @@ All icons that are used in the extensions can be found in `resources/icons`. A [build script](../scripts/generateIcons.ts) generates extension artifacts in [core/](../packages/core/): -- `resources/icons/cloud9/generated` - `resources/fonts/aws-toolkit-icons.woff` - `resources/css/icons.css` - `contributes.icons` in [amazonq package.json](../packages/amazonq/package.json) and [toolkit package.json](../packages/toolkit/package.json) @@ -31,7 +30,7 @@ If your desired icon does not work well as a font, see [Theme Overrides](#theme- ## Identifiers -Icons (except those in `cloud9`) can be referenced within the Toolkit by concatenating the icon path with hyphens, omitting the 'theme' if applicable. +Icons can be referenced within the Toolkit by concatenating the icon path with hyphens, omitting the 'theme' if applicable. Examples: @@ -50,11 +49,6 @@ For example, if I wanted to use a special App Runner service icon, then I need t - `resources/icons/aws/dark/apprunner-service.svg` - `resources/icons/aws/light/apprunner-service.svg` -A similar format is used for overriding icons only on Cloud9: - -- `resources/icons/cloud9/dark/aws-apprunner-service.svg` -- `resources/icons/cloud9/light/aws-apprunner-service.svg` - These icons will **not** be usuable as Codicons or as font icons. ## Third Party diff --git a/docs/web.md b/docs/web.md index 4da78f37236..1540ddf8134 100644 --- a/docs/web.md +++ b/docs/web.md @@ -16,7 +16,7 @@ You can run the Web mode implementation of the extension in the following ways. - To see logs, using the Command Palette search: `Toggle Developer Tools`. Then go to the `Console` tab. In web mode VS Code seems to duplicate log messages, idk how to fix this. - The difference between web mode and Node.js/desktop is that in web mode everything runs in browser environment so certain things like Node.js modules will **not** be available. -## Running in an actual Browser +## Running in an actual Browser (Recommended) The following steps will result in a Chrome window running with VS Code and the web version of the AWS Toolkit extension installed: @@ -43,7 +43,7 @@ and the web version of the AWS Toolkit extension installed: const context = browser ``` -2. In the `Run & Debug` menu select the `Extension (Chrome)` option +2. In the `Run & Debug` menu select the `Extension Web` option > Note: To stop the debug session, you need to click the read `Disconnect` button multiple times @@ -62,6 +62,12 @@ do the following: Now when you run the extension in the browser it will do CORS checks. +### Troubleshooting + +- `Extension Web` fails to launch + - Update `@vscode/test-web` by running `$ npm install @vscode/test-web@latest` + - Update/install playwright with `$ npx playwright install`. This will be mentioned in one of the build tasks outputs. + ## Running in [vscode.dev](https://vscode.dev) The following will explain how to get your latest local development changes running in the actual `vscode.dev`. Use this if you want to test on an actual VS Code Web instance. diff --git a/package-lock.json b/package-lock.json index 900f85f1ae8..5fa0d8b6865 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.289", + "@aws-toolkits/telemetry": "^1.0.295", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -31,7 +31,7 @@ "@typescript-eslint/parser": "^7.14.1", "@vscode/codicons": "^0.0.33", "@vscode/test-electron": "^2.3.8", - "@vscode/test-web": "^0.0.54", + "@vscode/test-web": "^0.0.65", "@vscode/vsce": "^2.19.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -2394,6 +2394,2214 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-docdb": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-docdb/-/client-docdb-3.726.0.tgz", + "integrity": "sha512-TJzvdBsNuly7IgcPuiwY75hrRqNS9c3YjYxPJzkb85nOfceUtLGz5XTRGvL6hRO3NG/0KfIlqrW0PKC3AitAFQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.726.0", + "@aws-sdk/client-sts": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-sdk-rds": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-docdb-elastic/-/client-docdb-elastic-3.726.0.tgz", + "integrity": "sha512-sROQQudocOJjyEBZsIVk4undxqHDYeGZSStbbVbxSghOH/zB9w+Sb+OKF9j5sCwXQ19TexY5N1UQ33i3nxxACw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.726.0", + "@aws-sdk/client-sts": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/client-sso": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.726.0.tgz", + "integrity": "sha512-NM5pjv2qglEc4XN3nnDqtqGsSGv1k5YTmzDo3W3pObItHmpS8grSeNfX9zSH+aVl0Q8hE4ZIgvTPNZ+GzwVlqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.726.0.tgz", + "integrity": "sha512-5JzTX9jwev7+y2Jkzjz0pd1wobB5JQfPOQF3N2DrJ5Pao0/k6uRYwE4NqB0p0HlGrMTDm7xNq7OSPPIPG575Jw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.726.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/client-sts": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.0.tgz", + "integrity": "sha512-047EqXv2BAn/43eP92zsozPnR3paFFMsj5gjytx9kGNtp+WV0fUZNztCOobtouAxBY0ZQ8Xx5RFnmjpRb6Kjsg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/core": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.723.0.tgz", + "integrity": "sha512-UraXNmvqj3vScSsTkjMwQkhei30BhXlW5WxX6JacMKVtl95c7z0qOXquTWeTalYkFfulfdirUhvSZrl+hcyqTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/core": "^3.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/signature-v4": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.723.0.tgz", + "integrity": "sha512-OuH2yULYUHTVDUotBoP/9AEUIJPn81GQ/YBtZLoo2QyezRJ2QiO/1epVtbJlhNZRwXrToLEDmQGA2QfC8c7pbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.723.0.tgz", + "integrity": "sha512-DTsKC6xo/kz/ZSs1IcdbQMTgiYbpGTGEd83kngFc1bzmw7AmK92DBZKNZpumf8R/UfSpTcj9zzUUmrWz1kD0eQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-stream": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.726.0.tgz", + "integrity": "sha512-seTtcKL2+gZX6yK1QRPr5mDJIBOatrpoyrO8D5b8plYtV/PDbDW3mtDJSWFHet29G61ZmlNElyXRqQCXn9WX+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-env": "3.723.0", + "@aws-sdk/credential-provider-http": "3.723.0", + "@aws-sdk/credential-provider-process": "3.723.0", + "@aws-sdk/credential-provider-sso": "3.726.0", + "@aws-sdk/credential-provider-web-identity": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.726.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.726.0.tgz", + "integrity": "sha512-jjsewBcw/uLi24x8JbnuDjJad4VA9ROCE94uVRbEnGmUEsds75FWOKp3fWZLQlmjLtzsIbJOZLALkZP86liPaw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.723.0", + "@aws-sdk/credential-provider-http": "3.723.0", + "@aws-sdk/credential-provider-ini": "3.726.0", + "@aws-sdk/credential-provider-process": "3.723.0", + "@aws-sdk/credential-provider-sso": "3.726.0", + "@aws-sdk/credential-provider-web-identity": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.723.0.tgz", + "integrity": "sha512-fgupvUjz1+jeoCBA7GMv0L6xEk92IN6VdF4YcFhsgRHlHvNgm7ayaoKQg7pz2JAAhG/3jPX6fp0ASNy+xOhmPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.726.0.tgz", + "integrity": "sha512-WxkN76WeB08j2yw7jUH9yCMPxmT9eBFd9ZA/aACG7yzOIlsz7gvG3P2FQ0tVg25GHM0E4PdU3p/ByTOawzcOAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/token-providers": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.723.0.tgz", + "integrity": "sha512-tl7pojbFbr3qLcOE6xWaNCf1zEfZrIdSJtOPeSXfV/thFMMAvIjgf3YN6Zo1a6cxGee8zrV/C8PgOH33n+Ev/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.723.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.723.0.tgz", + "integrity": "sha512-LLVzLvk299pd7v4jN9yOSaWDZDfH0SnBPb6q+FDPaOCMGBY8kuwQso7e/ozIKSmZHRMGO3IZrflasHM+rI+2YQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/middleware-logger": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.723.0.tgz", + "integrity": "sha512-chASQfDG5NJ8s5smydOEnNK7N0gDMyuPbx7dYYcm1t/PKtnVfvWF+DHCTrRC2Ej76gLJVCVizlAJKM8v8Kg3cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.723.0.tgz", + "integrity": "sha512-7usZMtoynT9/jxL/rkuDOFQ0C2mhXl4yCm67Rg7GNTstl67u7w5WN1aIRImMeztaKlw8ExjoTyo6WTs1Kceh7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.726.0.tgz", + "integrity": "sha512-hZvzuE5S0JmFie1r68K2wQvJbzyxJFdzltj9skgnnwdvLe8F/tz7MqLkm28uV0m4jeHk0LpiBo6eZaPkQiwsZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@smithy/core": "^3.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.723.0.tgz", + "integrity": "sha512-tGF/Cvch3uQjZIj34LY2mg8M2Dr4kYG8VU8Yd0dFnB1ybOEOveIK/9ypUo9ycZpB9oO6q01KRe5ijBaxNueUQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/token-providers": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.723.0.tgz", + "integrity": "sha512-hniWi1x4JHVwKElANh9afKIMUhAutHVBRD8zo6usr0PAoj+Waf220+1ULS74GXtLXAPCiNXl5Og+PHA7xT8ElQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.723.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/util-endpoints": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.726.0.tgz", + "integrity": "sha512-sLd30ASsPMoPn3XBK50oe/bkpJ4N8Bpb7SbhoxcY3Lk+fSASaWxbbXE81nbvCnkxrZCvkPOiDHzJCp1E2im71A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.723.0.tgz", + "integrity": "sha512-Wh9I6j2jLhNFq6fmXydIpqD1WyQLyTfSxjW9B+PXSnPyk3jtQW8AKQur7p97rO8LAUzVI0bv8kb3ZzDEVbquIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.726.0.tgz", + "integrity": "sha512-iEj6KX9o6IQf23oziorveRqyzyclWai95oZHDJtYav3fvLJKStwSjygO4xSF7ycHcTYeCHSLO1FFOHgGVs4Viw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz", + "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz", + "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.0.tgz", + "integrity": "sha512-swFv0wQiK7TGHeuAp6lfF5Kw1dHWsTrCuc+yh4Kh05gEShjsE2RUxHucEerR9ih9JITNtaHcSpUThn5Y/vDw0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz", + "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz", + "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/hash-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz", + "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz", + "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz", + "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.1.tgz", + "integrity": "sha512-hCCOPu9+sRI7Wj0rZKKnGylKXBEd9cQJetzjQqe8cT4PWvtQAbvNVa6cgAONiZg9m8LaXtP9/waxm3C3eO4hiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.0", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/middleware-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.1.tgz", + "integrity": "sha512-n3g2zZFgOWaz2ZYCy8+4wxSmq+HSTD8QKkRhFDv+nkxY1o7gzyp4PDz/+tOdcNPMPZ/A6Mt4aVECYNjQNiaHJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/middleware-serde": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.1.tgz", + "integrity": "sha512-Fh0E2SOF+S+P1+CsgKyiBInAt3o2b6Qk7YOp2W0Qx2XnfTdfMuSDKUEcnrtpxCzgKJnqXeLUZYqtThaP0VGqtA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz", + "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz", + "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/node-http-handler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.1.tgz", + "integrity": "sha512-ddQc7tvXiVLC5c3QKraGWde761KSk+mboCheZoWtuqnXh5l0WKyFy3NfDIM/dsKrI9HlLVH/21pi9wWK2gUFFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz", + "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz", + "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/service-error-classification": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz", + "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz", + "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/smithy-client": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.0.tgz", + "integrity": "sha512-NiboZnrsrZY+Cy5hQNbYi+nVNssXVi2I+yL4CIKNIanOhH8kpC5PKQ2jx/MQpwVr21a3XcVoQBArlpRF36OeEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.0", + "@smithy/middleware-endpoint": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz", + "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.1.tgz", + "integrity": "sha512-nkQifWzWUHw/D0aLPgyKut+QnJ5X+5E8wBvGfvrYLLZ86xPfVO6MoqfQo/9s4bF3Xscefua1M6KLZtobHMWrBg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.1.tgz", + "integrity": "sha512-LeAx2faB83litC9vaOdwFaldtto2gczUHxfFf8yoRwDU3cwL4/pDm7i0hxsuBCRk5mzHsrVGw+3EVCj32UZMdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz", + "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz", + "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.0.1.tgz", + "integrity": "sha512-Js16gOgU6Qht6qTPfuJgb+1YD4AEO+5Y1UPGWKSp3BNo8ONl/qhXSYDhFKJtwybRJynlCqvP5IeiaBsUmkSPTQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb-elastic/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/client-sso": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.726.0.tgz", + "integrity": "sha512-NM5pjv2qglEc4XN3nnDqtqGsSGv1k5YTmzDo3W3pObItHmpS8grSeNfX9zSH+aVl0Q8hE4ZIgvTPNZ+GzwVlqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.726.0.tgz", + "integrity": "sha512-5JzTX9jwev7+y2Jkzjz0pd1wobB5JQfPOQF3N2DrJ5Pao0/k6uRYwE4NqB0p0HlGrMTDm7xNq7OSPPIPG575Jw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.726.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/client-sts": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.0.tgz", + "integrity": "sha512-047EqXv2BAn/43eP92zsozPnR3paFFMsj5gjytx9kGNtp+WV0fUZNztCOobtouAxBY0ZQ8Xx5RFnmjpRb6Kjsg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-node": "3.726.0", + "@aws-sdk/middleware-host-header": "3.723.0", + "@aws-sdk/middleware-logger": "3.723.0", + "@aws-sdk/middleware-recursion-detection": "3.723.0", + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/region-config-resolver": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@aws-sdk/util-user-agent-browser": "3.723.0", + "@aws-sdk/util-user-agent-node": "3.726.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/core": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.723.0.tgz", + "integrity": "sha512-UraXNmvqj3vScSsTkjMwQkhei30BhXlW5WxX6JacMKVtl95c7z0qOXquTWeTalYkFfulfdirUhvSZrl+hcyqTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/core": "^3.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/signature-v4": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.723.0.tgz", + "integrity": "sha512-OuH2yULYUHTVDUotBoP/9AEUIJPn81GQ/YBtZLoo2QyezRJ2QiO/1epVtbJlhNZRwXrToLEDmQGA2QfC8c7pbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.723.0.tgz", + "integrity": "sha512-DTsKC6xo/kz/ZSs1IcdbQMTgiYbpGTGEd83kngFc1bzmw7AmK92DBZKNZpumf8R/UfSpTcj9zzUUmrWz1kD0eQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-stream": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.726.0.tgz", + "integrity": "sha512-seTtcKL2+gZX6yK1QRPr5mDJIBOatrpoyrO8D5b8plYtV/PDbDW3mtDJSWFHet29G61ZmlNElyXRqQCXn9WX+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/credential-provider-env": "3.723.0", + "@aws-sdk/credential-provider-http": "3.723.0", + "@aws-sdk/credential-provider-process": "3.723.0", + "@aws-sdk/credential-provider-sso": "3.726.0", + "@aws-sdk/credential-provider-web-identity": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.726.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.726.0.tgz", + "integrity": "sha512-jjsewBcw/uLi24x8JbnuDjJad4VA9ROCE94uVRbEnGmUEsds75FWOKp3fWZLQlmjLtzsIbJOZLALkZP86liPaw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.723.0", + "@aws-sdk/credential-provider-http": "3.723.0", + "@aws-sdk/credential-provider-ini": "3.726.0", + "@aws-sdk/credential-provider-process": "3.723.0", + "@aws-sdk/credential-provider-sso": "3.726.0", + "@aws-sdk/credential-provider-web-identity": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/credential-provider-imds": "^4.0.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.723.0.tgz", + "integrity": "sha512-fgupvUjz1+jeoCBA7GMv0L6xEk92IN6VdF4YcFhsgRHlHvNgm7ayaoKQg7pz2JAAhG/3jPX6fp0ASNy+xOhmPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.726.0.tgz", + "integrity": "sha512-WxkN76WeB08j2yw7jUH9yCMPxmT9eBFd9ZA/aACG7yzOIlsz7gvG3P2FQ0tVg25GHM0E4PdU3p/ByTOawzcOAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.726.0", + "@aws-sdk/core": "3.723.0", + "@aws-sdk/token-providers": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.723.0.tgz", + "integrity": "sha512-tl7pojbFbr3qLcOE6xWaNCf1zEfZrIdSJtOPeSXfV/thFMMAvIjgf3YN6Zo1a6cxGee8zrV/C8PgOH33n+Ev/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.723.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.723.0.tgz", + "integrity": "sha512-LLVzLvk299pd7v4jN9yOSaWDZDfH0SnBPb6q+FDPaOCMGBY8kuwQso7e/ozIKSmZHRMGO3IZrflasHM+rI+2YQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/middleware-logger": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.723.0.tgz", + "integrity": "sha512-chASQfDG5NJ8s5smydOEnNK7N0gDMyuPbx7dYYcm1t/PKtnVfvWF+DHCTrRC2Ej76gLJVCVizlAJKM8v8Kg3cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.723.0.tgz", + "integrity": "sha512-7usZMtoynT9/jxL/rkuDOFQ0C2mhXl4yCm67Rg7GNTstl67u7w5WN1aIRImMeztaKlw8ExjoTyo6WTs1Kceh7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.726.0.tgz", + "integrity": "sha512-hZvzuE5S0JmFie1r68K2wQvJbzyxJFdzltj9skgnnwdvLe8F/tz7MqLkm28uV0m4jeHk0LpiBo6eZaPkQiwsZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.723.0", + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-endpoints": "3.726.0", + "@smithy/core": "^3.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.723.0.tgz", + "integrity": "sha512-tGF/Cvch3uQjZIj34LY2mg8M2Dr4kYG8VU8Yd0dFnB1ybOEOveIK/9ypUo9ycZpB9oO6q01KRe5ijBaxNueUQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/token-providers": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.723.0.tgz", + "integrity": "sha512-hniWi1x4JHVwKElANh9afKIMUhAutHVBRD8zo6usr0PAoj+Waf220+1ULS74GXtLXAPCiNXl5Og+PHA7xT8ElQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.723.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/util-endpoints": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.726.0.tgz", + "integrity": "sha512-sLd30ASsPMoPn3XBK50oe/bkpJ4N8Bpb7SbhoxcY3Lk+fSASaWxbbXE81nbvCnkxrZCvkPOiDHzJCp1E2im71A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.723.0.tgz", + "integrity": "sha512-Wh9I6j2jLhNFq6fmXydIpqD1WyQLyTfSxjW9B+PXSnPyk3jtQW8AKQur7p97rO8LAUzVI0bv8kb3ZzDEVbquIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.726.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.726.0.tgz", + "integrity": "sha512-iEj6KX9o6IQf23oziorveRqyzyclWai95oZHDJtYav3fvLJKStwSjygO4xSF7ycHcTYeCHSLO1FFOHgGVs4Viw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.726.0", + "@aws-sdk/types": "3.723.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz", + "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz", + "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.0.tgz", + "integrity": "sha512-swFv0wQiK7TGHeuAp6lfF5Kw1dHWsTrCuc+yh4Kh05gEShjsE2RUxHucEerR9ih9JITNtaHcSpUThn5Y/vDw0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz", + "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz", + "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/hash-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz", + "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz", + "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz", + "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.1.tgz", + "integrity": "sha512-hCCOPu9+sRI7Wj0rZKKnGylKXBEd9cQJetzjQqe8cT4PWvtQAbvNVa6cgAONiZg9m8LaXtP9/waxm3C3eO4hiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.0", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/middleware-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.1.tgz", + "integrity": "sha512-n3g2zZFgOWaz2ZYCy8+4wxSmq+HSTD8QKkRhFDv+nkxY1o7gzyp4PDz/+tOdcNPMPZ/A6Mt4aVECYNjQNiaHJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/middleware-serde": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.1.tgz", + "integrity": "sha512-Fh0E2SOF+S+P1+CsgKyiBInAt3o2b6Qk7YOp2W0Qx2XnfTdfMuSDKUEcnrtpxCzgKJnqXeLUZYqtThaP0VGqtA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz", + "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz", + "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/node-http-handler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.1.tgz", + "integrity": "sha512-ddQc7tvXiVLC5c3QKraGWde761KSk+mboCheZoWtuqnXh5l0WKyFy3NfDIM/dsKrI9HlLVH/21pi9wWK2gUFFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz", + "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz", + "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/service-error-classification": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz", + "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz", + "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/smithy-client": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.0.tgz", + "integrity": "sha512-NiboZnrsrZY+Cy5hQNbYi+nVNssXVi2I+yL4CIKNIanOhH8kpC5PKQ2jx/MQpwVr21a3XcVoQBArlpRF36OeEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.0", + "@smithy/middleware-endpoint": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz", + "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.1.tgz", + "integrity": "sha512-nkQifWzWUHw/D0aLPgyKut+QnJ5X+5E8wBvGfvrYLLZ86xPfVO6MoqfQo/9s4bF3Xscefua1M6KLZtobHMWrBg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.1.tgz", + "integrity": "sha512-LeAx2faB83litC9vaOdwFaldtto2gczUHxfFf8yoRwDU3cwL4/pDm7i0hxsuBCRk5mzHsrVGw+3EVCj32UZMdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz", + "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz", + "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.0.1.tgz", + "integrity": "sha512-Js16gOgU6Qht6qTPfuJgb+1YD4AEO+5Y1UPGWKSp3BNo8ONl/qhXSYDhFKJtwybRJynlCqvP5IeiaBsUmkSPTQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-docdb/node_modules/@smithy/util-waiter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.2.tgz", + "integrity": "sha512-piUTHyp2Axx3p/kc2CIJkYSv0BAaheBQmbACZgQSSfWUumWNW+R1lL+H9PDBxKJkvOeEX+hKYEFiwO8xagL8AQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-lambda": { "version": "3.637.0", "license": "Apache-2.0", @@ -5386,114 +7594,487 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/middleware-host-header/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/types": { + "version": "3.3.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger/node_modules/@smithy/types": { + "version": "3.3.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/types": { + "version": "3.3.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-rds/-/middleware-sdk-rds-3.723.0.tgz", + "integrity": "sha512-2zN/xmWP/iBO3r638y4mwwuZOJ1h08rVc13wqS6RzhebKJZvzPWmT7leB8ejEPYHOtzTC1hk1vvZt/WVB4Qqjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@aws-sdk/util-format-url": "3.723.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/signature-v4": "^5.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz", + "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.0.tgz", + "integrity": "sha512-swFv0wQiK7TGHeuAp6lfF5Kw1dHWsTrCuc+yh4Kh05gEShjsE2RUxHucEerR9ih9JITNtaHcSpUThn5Y/vDw0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz", + "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.1.tgz", + "integrity": "sha512-hCCOPu9+sRI7Wj0rZKKnGylKXBEd9cQJetzjQqe8cT4PWvtQAbvNVa6cgAONiZg9m8LaXtP9/waxm3C3eO4hiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.1.0", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/middleware-serde": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.1.tgz", + "integrity": "sha512-Fh0E2SOF+S+P1+CsgKyiBInAt3o2b6Qk7YOp2W0Qx2XnfTdfMuSDKUEcnrtpxCzgKJnqXeLUZYqtThaP0VGqtA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz", + "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/node-http-handler": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.1.tgz", + "integrity": "sha512-ddQc7tvXiVLC5c3QKraGWde761KSk+mboCheZoWtuqnXh5l0WKyFy3NfDIM/dsKrI9HlLVH/21pi9wWK2gUFFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/property-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz", + "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz", + "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz", + "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/protocol-http": { - "version": "4.1.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/url-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz", + "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/types": { - "version": "3.3.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", "license": "Apache-2.0", "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.609.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.609.0", - "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-logger/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-logger/node_modules/@smithy/types": { - "version": "3.3.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.620.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.609.0", - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.0.1.tgz", + "integrity": "sha512-Js16gOgU6Qht6qTPfuJgb+1YD4AEO+5Y1UPGWKSp3BNo8ONl/qhXSYDhFKJtwybRJynlCqvP5IeiaBsUmkSPTQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/protocol-http": { - "version": "4.1.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/types": { - "version": "3.3.0", + "node_modules/@aws-sdk/middleware-sdk-rds/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", "license": "Apache-2.0", "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@aws-sdk/middleware-stack": { @@ -5886,6 +8467,72 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.723.0.tgz", + "integrity": "sha512-70+xUrdcnencPlCdV9XkRqmgj0vLDb8vm4mcEsgabg5QQ3S80KM0GEuhBAIGMkBWwNQTzCgQy2s7xOUlJPbu+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.723.0", + "@smithy/querystring-builder": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/util-hex-encoding": { "version": "3.46.0", "license": "Apache-2.0", @@ -6047,10 +8694,11 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.289", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.289.tgz", - "integrity": "sha512-srzr3JGMprOX2rrUAhribVBrUMfvR6uOhwksaxu63/GMTBjEWjwfcKzpgQzxu1+InmGioBa4zKdKKV/hAaUCmw==", + "version": "1.0.295", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.295.tgz", + "integrity": "sha512-NGBM5vhNNHwEhok3asXpUW7oZv/z8mjZaf34LGflqEh/5+VraTd76T+QBz18sC+nE2sPvhTO+zjptR9zg5bBUA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ajv": "^6.12.6", "cross-spawn": "^7.0.6", @@ -6061,19 +8709,6 @@ "yargs": "^17.0.1" } }, - "node_modules/@aws-toolkits/telemetry/node_modules/fs-extra": { - "version": "11.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@aws/fully-qualified-names": { "version": "2.1.4", "dev": true, @@ -6083,10 +8718,11 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.21.4.tgz", - "integrity": "sha512-sYeQHJ8yEQQQsre1soXQFebbqZFcXerIxJ/d9kg/YzZUauCirW7v/0f/kHs9y7xYkYGa8y3exV6b6e4+juO1DQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.22.1.tgz", + "integrity": "sha512-6mWD5Fp4VDVSKIv3sRKopoeh3GeiXEp2gWXmUWSVE9ccnnnavPyKSebV6vJiHJHtuS1da7i6ZLVednpsV9I49Q==", "hasInstallScript": true, + "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -6617,22 +9253,24 @@ } }, "node_modules/@koa/router": { - "version": "12.0.1", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^6.3.0" }, "engines": { - "node": ">= 12" + "node": ">= 18" } }, "node_modules/@koa/router/node_modules/path-to-regexp": { "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT" }, @@ -6693,26 +9331,17 @@ } }, "node_modules/@playwright/browser-chromium": { - "version": "1.43.1", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.49.1.tgz", + "integrity": "sha512-LLeyllKSucbojsJBOpdJshwW27ZXZs3oypqffkVWLUvxX2azHJMOevsOcWpjCfoYbpevkaEozM2xHeSUGF00lg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.43.1" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@playwright/browser-chromium/node_modules/playwright-core": { - "version": "1.43.1", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "playwright-core": "1.49.1" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@sindresorhus/is": { @@ -8874,6 +11503,8 @@ }, "node_modules/@ts-morph/common": { "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", + "integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==", "dev": true, "license": "MIT", "dependencies": { @@ -8885,6 +11516,8 @@ }, "node_modules/@ts-morph/common/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { @@ -8893,6 +11526,8 @@ }, "node_modules/@ts-morph/common/node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -8907,6 +11542,8 @@ }, "node_modules/@ts-morph/common/node_modules/mkdirp": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, "license": "MIT", "bin": { @@ -9576,10 +12213,6 @@ "version": "1.64.0", "license": "MIT" }, - "node_modules/@vscode/l10n": { - "version": "0.0.13", - "license": "MIT" - }, "node_modules/@vscode/test-electron": { "version": "2.3.8", "dev": true, @@ -9595,41 +12228,77 @@ } }, "node_modules/@vscode/test-web": { - "version": "0.0.54", + "version": "0.0.65", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.65.tgz", + "integrity": "sha512-jNc6FyJARgiru/2Y8vXSkaf399JFtTNxAAtwLPzSSU5C4+AJwvOOOhlVHQfmee51R9LIs6uwZryFxmqSfMhniQ==", "dev": true, "license": "MIT", "dependencies": { "@koa/cors": "^5.0.0", - "@koa/router": "^12.0.1", - "@playwright/browser-chromium": "^1.43.1", + "@koa/router": "^13.1.0", + "@playwright/browser-chromium": "^1.49.0", + "glob": "^11.0.0", "gunzip-maybe": "^1.4.2", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "koa": "^2.15.3", "koa-morgan": "^1.0.1", "koa-mount": "^4.0.0", "koa-static": "^5.0.0", "minimist": "^1.2.8", - "playwright": "^1.43.1", - "tar-fs": "^3.0.5", + "playwright": "^1.49.0", + "tar-fs": "^3.0.6", "vscode-uri": "^3.0.8" }, "bin": { - "vscode-test-web": "out/index.js" + "vscode-test-web": "out/server/index.js" }, "engines": { "node": ">=16" } }, "node_modules/@vscode/test-web/node_modules/agent-base": { - "version": "7.1.1", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vscode/test-web/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vscode/test-web/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">= 14" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@vscode/test-web/node_modules/http-proxy-agent": { @@ -9645,17 +12314,78 @@ } }, "node_modules/@vscode/test-web/node_modules/https-proxy-agent": { - "version": "7.0.4", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { "node": ">= 14" } }, + "node_modules/@vscode/test-web/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/test-web/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@vscode/test-web/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/test-web/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/test-web/node_modules/tar-fs": { "version": "3.0.6", "dev": true, @@ -10282,41 +13012,46 @@ "link": true }, "node_modules/amazon-states-language-service": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/amazon-states-language-service/-/amazon-states-language-service-1.13.0.tgz", - "integrity": "sha512-XT/7LL9+TRCB8H3t0kM6h2uivHa7Pn2lZGpvHKujH1MM+lQ7aaprAKrnZkfSk9++VFNbFJBAnKW+5NN2xVcvlA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/amazon-states-language-service/-/amazon-states-language-service-1.15.0.tgz", + "integrity": "sha512-FGWkQ88oy8o0VWFG+UJmJFwDODELQltWkl6P948/1CCnBWyergpW5Lcsa0+uYfls77shj0X6jsV+8rFk6fHYnQ==", "dependencies": { "js-yaml": "^4.1.0", - "vscode-json-languageservice": "5.3.5", - "vscode-languageserver": "^8.1.0", + "jsonata": "2.0.5", + "lodash": "^4.17.21", + "vscode-json-languageservice": "3.4.9", + "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.0", - "vscode-languageserver-types": "^3.15.1", + "vscode-languageserver-types": "^3.17.5", "yaml-language-server": "0.15.0" } }, "node_modules/amazon-states-language-service/node_modules/vscode-jsonrpc": { - "version": "8.1.0", - "license": "MIT", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "engines": { "node": ">=14.0.0" } }, "node_modules/amazon-states-language-service/node_modules/vscode-languageserver": { - "version": "8.1.0", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "dependencies": { - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/amazon-states-language-service/node_modules/vscode-languageserver-protocol": { - "version": "3.17.3", - "license": "MIT", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dependencies": { - "vscode-jsonrpc": "8.1.0", - "vscode-languageserver-types": "3.17.3" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "node_modules/ansi-colors": { @@ -10480,8 +13215,10 @@ "link": true }, "node_modules/aws-sdk": { - "version": "2.1384.0", - "license": "Apache-2.0", + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -10492,7 +13229,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" }, "engines": { "node": ">= 10.0.0" @@ -10505,17 +13242,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/aws-sdk/node_modules/xml2js": { - "version": "0.5.0", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/aws-ssm-document-language-service": { "version": "1.0.0", "license": "Apache-2.0", @@ -11491,10 +14217,11 @@ } }, "node_modules/code-block-writer": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.2.tgz", - "integrity": "sha512-XfXzAGiStXSmCIwrkdfvc7FS5Dtj8yelCtyOf2p2skCAfvLd6zu0rGzuS9NSCO3bq1JKpFZ7tbKdKlcd5occQA==", - "dev": true + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" }, "node_modules/color": { "version": "3.2.1", @@ -13694,6 +16421,21 @@ "license": "MIT", "optional": true }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-monkey": { "version": "1.0.3", "dev": true, @@ -15272,12 +18014,22 @@ "node": ">=6" } }, + "node_modules/jsonata": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.5.tgz", + "integrity": "sha512-wEse9+QLIIU5IaCgtJCPsFi/H4F3qcikWzF4bAELZiRz08ohfx3Q6CjDRf4ZPF5P/92RI3KIHtb7u3jqPaHXdQ==", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonc-parser": { "version": "3.2.0", "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16021,7 +18773,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -16757,6 +19511,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "dev": true, @@ -17046,31 +19807,35 @@ } }, "node_modules/playwright": { - "version": "1.44.0", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.44.0" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.44.0", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/pluralize": { @@ -19458,6 +22223,8 @@ }, "node_modules/ts-morph": { "version": "23.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", + "integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==", "dev": true, "license": "MIT", "dependencies": { @@ -19721,7 +22488,9 @@ "license": "MIT" }, "node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -19919,19 +22688,26 @@ } }, "node_modules/vscode-json-languageservice": { - "version": "5.3.5", - "license": "MIT", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-3.4.9.tgz", + "integrity": "sha512-4VCpZ9ooea/Zc/MTnj1ccc9C7rqcoinKVQLhLoi6jw6yueSf4y4tg/YIUiPPVMlEAG7ZCPS+NVmqxisQ+mOsSw==", "dependencies": { - "@vscode/l10n": "^0.0.13", - "jsonc-parser": "^3.2.0", - "vscode-languageserver-textdocument": "^1.0.8", - "vscode-languageserver-types": "^3.17.3", - "vscode-uri": "^3.0.7" + "jsonc-parser": "^2.2.0", + "vscode-languageserver-textdocument": "^1.0.0-next.4", + "vscode-languageserver-types": "^3.15.0-next.6", + "vscode-nls": "^4.1.1", + "vscode-uri": "^2.1.0" } }, - "node_modules/vscode-json-languageservice/node_modules/vscode-uri": { - "version": "3.0.7", - "license": "MIT" + "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" + }, + "node_modules/vscode-json-languageservice/node_modules/vscode-nls": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.2.tgz", + "integrity": "sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==" }, "node_modules/vscode-jsonrpc": { "version": "5.0.1", @@ -19985,8 +22761,9 @@ "license": "MIT" }, "node_modules/vscode-languageserver-types": { - "version": "3.17.3", - "license": "MIT" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-nls": { "version": "5.2.0", @@ -20820,8 +23597,9 @@ } }, "node_modules/xml2js": { - "version": "0.6.1", - "license": "MIT", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -21126,7 +23904,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.43.0-SNAPSHOT", + "version": "1.46.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -21147,6 +23925,8 @@ "@aws-sdk/client-cloudformation": "^3.667.0", "@aws-sdk/client-cloudwatch-logs": "^3.666.0", "@aws-sdk/client-cognito-identity": "^3.637.0", + "@aws-sdk/client-docdb": "^3.699.0", + "@aws-sdk/client-docdb-elastic": "^3.699.0", "@aws-sdk/client-lambda": "^3.637.0", "@aws-sdk/client-sso": "^3.342.0", "@aws-sdk/client-sso-oidc": "^3.574.0", @@ -21156,7 +23936,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "^4.21.4", + "@aws/mynah-ui": "^4.22.1", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^2.3.1", @@ -21167,9 +23947,9 @@ "@vscode/debugprotocol": "^1.57.0", "@zip.js/zip.js": "^2.7.41", "adm-zip": "^0.5.10", - "amazon-states-language-service": "^1.13.0", + "amazon-states-language-service": "^1.15.0", "async-lock": "^1.4.0", - "aws-sdk": "^2.1384.0", + "aws-sdk": "^2.1692.0", "aws-ssm-document-language-service": "^1.0.0", "bytes": "^3.1.2", "cross-fetch": "^4.0.0", @@ -21286,7 +24066,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.42.0-SNAPSHOT", + "version": "3.45.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index cb669449f9d..f3b9b4a308c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "mergeReports": "ts-node ./scripts/mergeReports.ts" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.289", + "@aws-toolkits/telemetry": "^1.0.295", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -51,7 +51,7 @@ "@typescript-eslint/parser": "^7.14.1", "@vscode/codicons": "^0.0.33", "@vscode/test-electron": "^2.3.8", - "@vscode/test-web": "^0.0.54", + "@vscode/test-web": "^0.0.65", "@vscode/vsce": "^2.19.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", diff --git a/packages/amazonq/.changes/1.43.0.json b/packages/amazonq/.changes/1.43.0.json new file mode 100644 index 00000000000..a4f2376f2e6 --- /dev/null +++ b/packages/amazonq/.changes/1.43.0.json @@ -0,0 +1,42 @@ +{ + "date": "2025-01-15", + "version": "1.43.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Auth: Valid StartURL not accepted at login" + }, + { + "type": "Bug Fix", + "description": "Fix inline completion supplementalContext length exceeding maximum in certain cases" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /test: Unit test generation completed message shows after accept/reject action" + }, + { + "type": "Bug Fix", + "description": "/test: for unsupported languages was sometimes unreliable" + }, + { + "type": "Bug Fix", + "description": "User-selected customizations are sometimes not being persisted." + }, + { + "type": "Bug Fix", + "description": "Amazon q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining" + }, + { + "type": "Feature", + "description": "Adds capability to send new context commands to AB groups" + }, + { + "type": "Feature", + "description": "feat(amazonq): Add error message for updated README too large" + }, + { + "type": "Feature", + "description": "Enhance Q inline completion context fetching for better suggestion quality" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.44.0.json b/packages/amazonq/.changes/1.44.0.json new file mode 100644 index 00000000000..7593e9a7af3 --- /dev/null +++ b/packages/amazonq/.changes/1.44.0.json @@ -0,0 +1,50 @@ +{ + "date": "2025-01-23", + "version": "1.44.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q: word duplication when pressing tab on context selector fixed" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /doc: Prevent users from requesting changes if no iterations remain" + }, + { + "type": "Bug Fix", + "description": "`/test`: view diffs by clicking files in the file tree, aligning the behavior with the 'View Diff' button." + }, + { + "type": "Bug Fix", + "description": "/review: Improved error handling for code fix operations" + }, + { + "type": "Bug Fix", + "description": "Amazon Q: cursor no longer jumps after navigating prompt history" + }, + { + "type": "Bug Fix", + "description": "Improve the text description of workspace index settings" + }, + { + "type": "Bug Fix", + "description": "Notifications: 'Dismiss' command visible in command palette." + }, + { + "type": "Bug Fix", + "description": "/transform: replace icons in Transformation Hub with text" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /doc: Ask for user prompt if error occurs while updating documentation" + }, + { + "type": "Feature", + "description": "Amazon Q: increase chat current active file context char limit to 40k" + }, + { + "type": "Feature", + "description": "/review: Code issues can be grouped by file location or severity" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.45.0-SNAPSHOT.json b/packages/amazonq/.changes/1.45.0-SNAPSHOT.json new file mode 100644 index 00000000000..881d3e94e5a --- /dev/null +++ b/packages/amazonq/.changes/1.45.0-SNAPSHOT.json @@ -0,0 +1,18 @@ +{ + "date": "2025-01-30", + "version": "1.45.0-SNAPSHOT", + "entries": [ + { + "type": "Bug Fix", + "description": "Allow AB users with an overridden customization to go back to the default customization" + }, + { + "type": "Bug Fix", + "description": "For security reasons, disabled auto linkify for link texts coming in markdown other than [TEXT](URL) format" + }, + { + "type": "Feature", + "description": "Add setting to allow Q /dev to run code and test commands" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.45.0.json b/packages/amazonq/.changes/1.45.0.json new file mode 100644 index 00000000000..7ef04691624 --- /dev/null +++ b/packages/amazonq/.changes/1.45.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-01-30", + "version": "1.45.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Allow AB users with an overridden customization to go back to the default customization" + }, + { + "type": "Bug Fix", + "description": "For security reasons, disabled auto linkify for link texts coming in markdown other than [TEXT](URL) format" + }, + { + "type": "Feature", + "description": "Add setting to allow Q /dev to run code and test commands" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-22f55c14-bd62-49d7-aa68-3ab3cc8f0929.json b/packages/amazonq/.changes/next-release/Bug Fix-22f55c14-bd62-49d7-aa68-3ab3cc8f0929.json new file mode 100644 index 00000000000..ba2818371ba --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-22f55c14-bd62-49d7-aa68-3ab3cc8f0929.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Citation links are not clickable as numbers, but appear as non-clickable texts" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-5c9f7fbc-1333-4c28-a049-7b17a17a7cea.json b/packages/amazonq/.changes/next-release/Bug Fix-5c9f7fbc-1333-4c28-a049-7b17a17a7cea.json new file mode 100644 index 00000000000..783359087ea --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-5c9f7fbc-1333-4c28-a049-7b17a17a7cea.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Fix language server start failure in AL2023 ARM64" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-ca03bdf5-a707-479e-9405-e9ac3b73e30a.json b/packages/amazonq/.changes/next-release/Bug Fix-ca03bdf5-a707-479e-9405-e9ac3b73e30a.json new file mode 100644 index 00000000000..4399da617e7 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-ca03bdf5-a707-479e-9405-e9ac3b73e30a.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q: Fix code upload error when using /dev or /doc on Remote SSH" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json b/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json deleted file mode 100644 index 3f2f6330b2f..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "User-selected customizations are sometimes not being persisted." -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-dc78e630-6320-4297-9ae9-7a19891f5357.json b/packages/amazonq/.changes/next-release/Bug Fix-dc78e630-6320-4297-9ae9-7a19891f5357.json new file mode 100644 index 00000000000..e34722b9aac --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-dc78e630-6320-4297-9ae9-7a19891f5357.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "/test placeholder text aligned across IDEs" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-e2bb3c83-52e8-4ae9-aedb-844e8780971b.json b/packages/amazonq/.changes/next-release/Bug Fix-e2bb3c83-52e8-4ae9-aedb-844e8780971b.json new file mode 100644 index 00000000000..55ce7a614a9 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-e2bb3c83-52e8-4ae9-aedb-844e8780971b.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Inline: Typos in the first example suggestion" +} diff --git a/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json b/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json deleted file mode 100644 index 7e1d6e86caa..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "feat(amazonq): Add error message for updated README too large" -} diff --git a/packages/amazonq/.changes/next-release/Feature-b63d765a-24f9-44e6-b826-b905f35e67d1.json b/packages/amazonq/.changes/next-release/Feature-b63d765a-24f9-44e6-b826-b905f35e67d1.json new file mode 100644 index 00000000000..595e48f0026 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-b63d765a-24f9-44e6-b826-b905f35e67d1.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Inline suggestions: Pre-fetch recommendations to reduce suggestion latency." +} diff --git a/packages/amazonq/.changes/next-release/Feature-d4b1f56f-fbfd-43e9-b4e1-e2c972dcf197.json b/packages/amazonq/.changes/next-release/Feature-d4b1f56f-fbfd-43e9-b4e1-e2c972dcf197.json new file mode 100644 index 00000000000..065aad75325 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-d4b1f56f-fbfd-43e9-b4e1-e2c972dcf197.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Added github issue link and description to the chat answer feedback form" +} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 9cfaa7fe04f..8e8dd546b83 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,35 @@ +## 1.45.0 2025-01-30 + +- **Bug Fix** Allow AB users with an overridden customization to go back to the default customization +- **Bug Fix** For security reasons, disabled auto linkify for link texts coming in markdown other than [TEXT](URL) format +- **Feature** Add setting to allow Q /dev to run code and test commands + +## 1.44.0 2025-01-23 + +- **Bug Fix** Amazon Q: word duplication when pressing tab on context selector fixed +- **Bug Fix** Amazon Q /doc: Prevent users from requesting changes if no iterations remain +- **Bug Fix** `/test`: view diffs by clicking files in the file tree, aligning the behavior with the 'View Diff' button. +- **Bug Fix** /review: Improved error handling for code fix operations +- **Bug Fix** Amazon Q: cursor no longer jumps after navigating prompt history +- **Bug Fix** Improve the text description of workspace index settings +- **Bug Fix** Notifications: 'Dismiss' command visible in command palette. +- **Bug Fix** /transform: replace icons in Transformation Hub with text +- **Bug Fix** Amazon Q /doc: Ask for user prompt if error occurs while updating documentation +- **Feature** Amazon Q: increase chat current active file context char limit to 40k +- **Feature** /review: Code issues can be grouped by file location or severity + +## 1.43.0 2025-01-15 + +- **Bug Fix** Auth: Valid StartURL not accepted at login +- **Bug Fix** Fix inline completion supplementalContext length exceeding maximum in certain cases +- **Bug Fix** Amazon Q /test: Unit test generation completed message shows after accept/reject action +- **Bug Fix** /test: for unsupported languages was sometimes unreliable +- **Bug Fix** User-selected customizations are sometimes not being persisted. +- **Bug Fix** Amazon q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining +- **Feature** Adds capability to send new context commands to AB groups +- **Feature** feat(amazonq): Add error message for updated README too large +- **Feature** Enhance Q inline completion context fetching for better suggestion quality + ## 1.42.0 2025-01-09 - **Bug Fix** Amazon Q /doc: Improve button text phrasing diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 8981ba83502..62f008a2213 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.43.0-SNAPSHOT", + "version": "1.46.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -127,6 +127,11 @@ "markdownDescription": "%AWS.configuration.description.amazonq%", "default": true }, + "amazonQ.allowFeatureDevelopmentToRunCodeAndTests": { + "markdownDescription": "%AWS.configuration.description.featureDevelopment.allowRunningCodeAndTests%", + "type": "object", + "default": {} + }, "amazonQ.importRecommendationForInlineCodeSuggestions": { "type": "boolean", "description": "%AWS.configuration.description.amazonq.importRecommendation%", @@ -198,7 +203,7 @@ { "id": "aws.amazonq.notifications", "name": "%AWS.notifications.title%", - "when": "!isCloud9 && !aws.isSageMaker && aws.amazonq.notifications.show" + "when": "!(isCloud9 || aws.isSageMaker) && aws.amazonq.notifications.show" }, { "type": "webview", @@ -314,16 +319,14 @@ "group": "navigation@3" }, { - "command": "aws.amazonq.transformationHub.summary.reveal", - "when": "view == aws.amazonq.transformationHub" - }, - { - "command": "aws.amazonq.transformationHub.reviewChanges.reveal", - "when": "view == aws.amazonq.transformationHub" + "command": "aws.amazonq.showTransformationPlanInHub", + "when": "view == aws.amazonq.transformationHub", + "group": "navigation@4" }, { - "command": "aws.amazonq.showTransformationPlanInHub", - "when": "view == aws.amazonq.transformationHub" + "command": "aws.amazonq.transformationHub.summary.reveal", + "when": "view == aws.amazonq.transformationHub", + "group": "navigation@5" }, { "command": "aws.amazonq.transformationHub.reviewChanges.acceptChanges", @@ -365,10 +368,15 @@ "when": "view == aws.AmazonQChatView || view == aws.amazonq.AmazonCommonAuth", "group": "y_toolkitMeta@2" }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "when": "view == aws.amazonq.SecurityIssuesTree", + "group": "navigation@1" + }, { "command": "aws.amazonq.security.showFilters", "when": "view == aws.amazonq.SecurityIssuesTree", - "group": "navigation" + "group": "navigation@2" } ], "view/item/context": [ @@ -493,7 +501,7 @@ "command": "_aws.amazonq.notifications.dismiss", "title": "%AWS.generic.dismiss%", "category": "%AWS.amazonq.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", + "enablement": "view == aws.amazonq.notifications", "icon": "$(remove-close)" }, { @@ -563,11 +571,6 @@ "title": "%AWS.command.q.transform.showChangeSummary%", "enablement": "gumby.isSummaryAvailable" }, - { - "command": "aws.amazonq.transformationHub.reviewChanges.reveal", - "title": "%AWS.command.q.transform.showChanges%", - "enablement": "gumby.reviewState == InReview" - }, { "command": "aws.amazonq.showTransformationPlanInHub", "title": "%AWS.command.q.transform.showTransformationPlan%", @@ -638,19 +641,16 @@ }, { "command": "aws.amazonq.stopTransformationInHub", - "title": "Stop Transformation", - "icon": "$(stop)", + "title": "%AWS.command.q.transform.stopJobInHub%", "enablement": "gumby.isStopButtonAvailable" }, { "command": "aws.amazonq.showPlanProgressInHub", - "title": "Show Transformation Status", - "icon": "$(checklist)" + "title": "%AWS.command.q.transform.viewJobProgress%" }, { "command": "aws.amazonq.showHistoryInHub", - "title": "Show Job Status", - "icon": "$(history)" + "title": "%AWS.command.q.transform.viewJobStatus%" }, { "command": "aws.amazonq.selectCustomization", @@ -724,6 +724,12 @@ { "command": "aws.amazonq.security.showFilters", "title": "%AWS.command.amazonq.filterIssues%", + "icon": "$(filter)", + "enablement": "view == aws.amazonq.SecurityIssuesTree" + }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "title": "%AWS.command.amazonq.groupIssues%", "icon": "$(list-filter)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 4ffa6e1e1de..7ace8d0095e 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -13,6 +13,7 @@ import { computeDecorations } from '../decorations/computeDecorations' import { CodelensProvider } from '../codeLenses/codeLenseProvider' import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' +import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer' import { codicon, getIcon, @@ -84,6 +85,7 @@ export class InlineChatController { await this.updateTaskAndLenses(task) this.referenceLogController.addReferenceLog(task.codeReferences, task.replacement ? task.replacement : '') await this.reset() + UserWrittenCodeTracker.instance.onQFinishesEdits() } public async rejectAllChanges(task = this.task, userInvoked: boolean): Promise { @@ -199,7 +201,7 @@ export class InlineChatController { getLogger().info('inlineQuickPick query is empty') return } - + UserWrittenCodeTracker.instance.onQStartsMakingEdits() this.userQuery = query await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) diff --git a/packages/amazonq/test/e2e/amazonq/assert.ts b/packages/amazonq/test/e2e/amazonq/assert.ts index 7bc7bb2c22e..5bcec3fc0b4 100644 --- a/packages/amazonq/test/e2e/amazonq/assert.ts +++ b/packages/amazonq/test/e2e/amazonq/assert.ts @@ -28,3 +28,14 @@ export function assertQuickActions(tab: Messenger, commands: string[]) { assert.fail(`Could not find commands: ${missingCommands.join(', ')} for ${tab.tabID}`) } } + +export function assertContextCommands(tab: Messenger, contextCommands: string[]) { + assert.deepStrictEqual( + tab + .getStore() + .contextCommands?.map((x) => x.commands) + .flat() + .map((x) => x.command), + contextCommands + ) +} diff --git a/packages/amazonq/test/e2e/amazonq/chat.test.ts b/packages/amazonq/test/e2e/amazonq/chat.test.ts new file mode 100644 index 00000000000..3021be28782 --- /dev/null +++ b/packages/amazonq/test/e2e/amazonq/chat.test.ts @@ -0,0 +1,85 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { qTestingFramework } from './framework/framework' +import sinon from 'sinon' +import { Messenger } from './framework/messenger' +import { MynahUIDataModel } from '@aws/mynah-ui' +import { assertContextCommands, assertQuickActions } from './assert' +import { registerAuthHook, using } from 'aws-core-vscode/test' +import { loginToIdC } from './utils/setup' +import { webviewConstants } from 'aws-core-vscode/amazonq' + +describe('Amazon Q Chat', function () { + let framework: qTestingFramework + let tab: Messenger + let store: MynahUIDataModel + + const availableCommands: string[] = ['/dev', '/test', '/review', '/doc', '/transform'] + + before(async function () { + /** + * Login to the amazonq-test-account. When running in CI this has unlimited + * calls to the backend api + */ + await using(registerAuthHook('amazonq-test-account'), async () => { + await loginToIdC() + }) + }) + + // jscpd:ignore-start + beforeEach(() => { + // Make sure you're logged in before every test + registerAuthHook('amazonq-test-account') + framework = new qTestingFramework('cwc', true, []) + tab = framework.createTab() + store = tab.getStore() + }) + + afterEach(() => { + framework.removeTab(tab.tabID) + framework.dispose() + sinon.restore() + }) + + it(`Shows quick actions: ${availableCommands.join(', ')}`, async () => { + assertQuickActions(tab, availableCommands) + }) + + it('Shows @workspace', () => { + assertContextCommands(tab, ['@workspace']) + }) + + // jscpd:ignore-end + + it('Shows title', () => { + assert.deepStrictEqual(store.tabTitle, 'Chat') + }) + + it('Shows placeholder', () => { + assert.deepStrictEqual(store.promptInputPlaceholder, 'Ask a question or enter "/" for quick actions') + }) + + it('Sends message', async () => { + tab.addChatMessage({ + prompt: 'What is a lambda', + }) + await tab.waitForChatFinishesLoading() + const chatItems = tab.getChatItems() + // the last item should be an answer + assert.deepStrictEqual(chatItems[4].type, 'answer') + }) + + describe('Clicks examples', () => { + it('Click help', async () => { + tab.clickButton('help') + await tab.waitForText(webviewConstants.helpMessage) + const chatItems = tab.getChatItems() + assert.deepStrictEqual(chatItems[4].type, 'answer') + assert.deepStrictEqual(chatItems[4].body, webviewConstants.helpMessage) + }) + }) +}) diff --git a/packages/amazonq/test/e2e/amazonq/doc.test.ts b/packages/amazonq/test/e2e/amazonq/doc.test.ts index 78322b63ab0..343d228c261 100644 --- a/packages/amazonq/test/e2e/amazonq/doc.test.ts +++ b/packages/amazonq/test/e2e/amazonq/doc.test.ts @@ -108,6 +108,71 @@ describe('Amazon Q Doc', async function () { FollowUpTypes.MakeChanges, FollowUpTypes.RejectChanges, ]) + + tab.clickButton(FollowUpTypes.AcceptChanges) + + await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) + }) + }) + + describe('Edits a README', () => { + beforeEach(async function () { + tab.addChatMessage({ command: '/doc' }) + await tab.waitForChatFinishesLoading() + }) + + it('Make specific change in README', async () => { + await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) + + tab.clickButton(FollowUpTypes.UpdateDocumentation) + + await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) + + tab.clickButton(FollowUpTypes.EditDocumentation) + + await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) + + tab.clickButton(FollowUpTypes.ProceedFolderSelection) + + tab.addChatMessage({ prompt: 'remove the repository structure section' }) + + await tab.waitForText( + `${i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${i18n('AWS.amazonq.doc.answer.codeResult')}` + ) + + await tab.waitForButtons([ + FollowUpTypes.AcceptChanges, + FollowUpTypes.MakeChanges, + FollowUpTypes.RejectChanges, + ]) + }) + + it('Handle unrelated prompt error', async () => { + await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) + + tab.clickButton(FollowUpTypes.UpdateDocumentation) + + await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) + + tab.clickButton(FollowUpTypes.EditDocumentation) + + await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) + + tab.clickButton(FollowUpTypes.ProceedFolderSelection) + + tab.addChatMessage({ prompt: 'tell me about the weather' }) + + await tab.waitForEvent(() => + tab.getChatItems().some(({ body }) => body?.startsWith(i18n('AWS.amazonq.doc.error.promptUnrelated'))) + ) + + await tab.waitForEvent(() => { + const store = tab.getStore() + return ( + !store.promptInputDisabledState && + store.promptInputPlaceholder === i18n('AWS.amazonq.doc.placeholder.editReadme') + ) + }) }) }) }) diff --git a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts index cc1670ced8f..d40ff72a0a9 100644 --- a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts +++ b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts @@ -161,7 +161,8 @@ describe('Amazon Q Feature Dev', function () { }) }) - describe('/dev {msg} entry', async () => { + // Disable failing tests while investigation. The tests are only failing in CI environments. + describe.skip('/dev {msg} entry', async () => { beforeEach(async function () { tab.addChatMessage({ command: '/dev', prompt }) await retryIfRequired( @@ -216,7 +217,7 @@ describe('Amazon Q Feature Dev', function () { }) }) - describe('file-level accepts', async () => { + describe.skip('file-level accepts', async () => { beforeEach(async function () { tab.addChatMessage({ command: '/dev', prompt: fileLevelAcceptPrompt }) await retryIfRequired( diff --git a/packages/amazonq/test/e2e/amazonq/framework/framework.ts b/packages/amazonq/test/e2e/amazonq/framework/framework.ts index b39dbe4314b..6a29015c06f 100644 --- a/packages/amazonq/test/e2e/amazonq/framework/framework.ts +++ b/packages/amazonq/test/e2e/amazonq/framework/framework.ts @@ -29,7 +29,7 @@ export class qTestingFramework { featureName: TabType, amazonQEnabled: boolean, featureConfigsSerialized: [string, FeatureContext][], - welcomeCount = 0 + welcomeCount = Number.MAX_VALUE // by default don't show the welcome page ) { /** * Instantiate the UI and override the postMessage to publish using the app message diff --git a/packages/amazonq/test/e2e/amazonq/template.test.ts b/packages/amazonq/test/e2e/amazonq/template.test.ts new file mode 100644 index 00000000000..42857575583 --- /dev/null +++ b/packages/amazonq/test/e2e/amazonq/template.test.ts @@ -0,0 +1,67 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// jscpd:ignore-start +import assert from 'assert' +import { qTestingFramework } from './framework/framework' +import sinon from 'sinon' +import { Messenger } from './framework/messenger' +import { MynahUIDataModel } from '@aws/mynah-ui' +import { assertQuickActions } from './assert' +import { registerAuthHook, using } from 'aws-core-vscode/test' +import { loginToIdC } from './utils/setup' + +describe.skip('Amazon Q Test Template', function () { + let framework: qTestingFramework + let tab: Messenger + let store: MynahUIDataModel + + const availableCommands: string[] = [] + + before(async function () { + /** + * Login to the amazonq-test-account. When running in CI this has unlimited + * calls to the backend api + */ + await using(registerAuthHook('amazonq-test-account'), async () => { + await loginToIdC() + }) + }) + + beforeEach(() => { + // Make sure you're logged in before every test + registerAuthHook('amazonq-test-account') + + // TODO change unknown to the tab type you want to test + framework = new qTestingFramework('unknown', true, []) + tab = framework.getTabs()[0] // use the default tab that gets created + framework.createTab() // alternatively you can create a new tab + store = tab.getStore() + }) + + afterEach(() => { + framework.removeTab(tab.tabID) + framework.dispose() + sinon.restore() + }) + + it(`Shows quick actions: ${availableCommands.join(', ')}`, async () => { + assertQuickActions(tab, availableCommands) + }) + + it('Shows title', () => { + assert.deepStrictEqual(store.tabTitle, '') + }) + + it('Shows placeholder', () => { + assert.deepStrictEqual(store.promptInputPlaceholder, '') + }) + + describe('clicks examples', () => {}) + + describe('sends message', async () => {}) +}) + +// jscpd:ignore-end diff --git a/packages/amazonq/test/e2e/amazonq/testGen.test.ts b/packages/amazonq/test/e2e/amazonq/testGen.test.ts new file mode 100644 index 00000000000..c78a9b9e087 --- /dev/null +++ b/packages/amazonq/test/e2e/amazonq/testGen.test.ts @@ -0,0 +1,219 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import vscode from 'vscode' +import { qTestingFramework } from './framework/framework' +import sinon from 'sinon' +import { Messenger } from './framework/messenger' +import { FollowUpTypes } from 'aws-core-vscode/amazonq' +import { registerAuthHook, using, TestFolder } from 'aws-core-vscode/test' +import { loginToIdC } from './utils/setup' +import { waitUntil, workspaceUtils } from 'aws-core-vscode/shared' + +describe('Amazon Q Test Generation', function () { + let framework: qTestingFramework + let tab: Messenger + + const testFiles = [ + { + language: 'python', + filePath: 'python3.7-image-sam-app/hello_world/app.py', + }, + { + language: 'java', + filePath: 'java17-gradle/HelloWorldFunction/src/main/java/helloworld/App.java', + }, + ] + + const unsupportedLanguages = [ + // move these over to testFiles once these languages are supported + // must be atleast one unsupported language here for testing + { + language: 'typescript', + filePath: 'ts-plain-sam-app/src/app.ts', + }, + { + language: 'javascript', + filePath: 'js-plain-sam-app/src/app.js', + }, + ] + + async function setupTestDocument(filePath: string, language: string) { + const document = await waitUntil(async () => { + const doc = await workspaceUtils.openTextDocument(filePath) + return doc + }, {}) + + if (!document) { + assert.fail(`Failed to open ${language} file`) + } + + await vscode.window.showTextDocument(document, { preview: false }) + + const activeEditor = vscode.window.activeTextEditor + if (!activeEditor || activeEditor.document !== document) { + assert.fail(`Failed to make temp file active`) + } + } + + async function waitForChatItems(index: number) { + await tab.waitForEvent(() => tab.getChatItems().length > index, { + waitTimeoutInMs: 5000, + waitIntervalInMs: 1000, + }) + } + + before(async function () { + await using(registerAuthHook('amazonq-test-account'), async () => { + await loginToIdC() + }) + }) + + beforeEach(async () => { + registerAuthHook('amazonq-test-account') + framework = new qTestingFramework('testgen', true, []) + tab = framework.createTab() + }) + + afterEach(async () => { + // Close all editors to prevent conflicts with subsequent tests trying to open the same file + await vscode.commands.executeCommand('workbench.action.closeAllEditors') + framework.removeTab(tab.tabID) + framework.dispose() + sinon.restore() + }) + + describe('Quick action availability', () => { + it('Shows /test when test generation is enabled', async () => { + const command = tab.findCommand('/test') + if (!command.length) { + assert.fail('Could not find command') + } + if (command.length > 1) { + assert.fail('Found too many commands with the name /test') + } + }) + + it('Does NOT show /test when test generation is NOT enabled', () => { + // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages + framework.dispose() + framework = new qTestingFramework('testgen', false, []) + const tab = framework.createTab() + const command = tab.findCommand('/test') + if (command.length > 0) { + assert.fail('Found command when it should not have been found') + } + }) + }) + + describe('/test entry', () => { + describe('Unsupported language', () => { + const { language, filePath } = unsupportedLanguages[0] + + beforeEach(async () => { + await setupTestDocument(filePath, language) + }) + + it(`/test for unsupported language redirects to chat`, async () => { + tab.addChatMessage({ command: '/test' }) + await tab.waitForChatFinishesLoading() + + await waitForChatItems(3) + const unsupportedLanguageMessage = tab.getChatItems()[3] + + assert.deepStrictEqual(unsupportedLanguageMessage.type, 'answer') + assert.deepStrictEqual( + unsupportedLanguageMessage.body, + `I'm sorry, but /test only supports Python and Java
While ${language.charAt(0).toUpperCase() + language.slice(1)} is not supported, I will generate a suggestion below.` + ) + }) + }) + + describe('External file', async () => { + let testFolder: TestFolder + let fileName: string + + beforeEach(async () => { + testFolder = await TestFolder.create() + fileName = 'test.py' + const filePath = await testFolder.write(fileName, 'def add(a, b): return a + b') + + const document = await vscode.workspace.openTextDocument(filePath) + await vscode.window.showTextDocument(document, { preview: false }) + }) + + it('/test for external file redirects to chat', async () => { + tab.addChatMessage({ command: '/test' }) + await tab.waitForChatFinishesLoading() + + await waitForChatItems(3) + const externalFileMessage = tab.getChatItems()[3] + + assert.deepStrictEqual(externalFileMessage.type, 'answer') + assert.deepStrictEqual( + externalFileMessage.body, + `I can't generate tests for ${fileName} because the file is outside of workspace scope.
I can still provide examples, instructions and code suggestions.` + ) + }) + }) + + for (const { language, filePath } of testFiles) { + describe(`${language} file`, () => { + beforeEach(async () => { + await setupTestDocument(filePath, language) + + tab.addChatMessage({ command: '/test' }) + await tab.waitForChatFinishesLoading() + + await tab.waitForButtons([FollowUpTypes.ViewDiff]) + tab.clickButton(FollowUpTypes.ViewDiff) + await tab.waitForChatFinishesLoading() + }) + + describe('View diff', async () => { + it('Clicks on view diff', async () => { + const chatItems = tab.getChatItems() + const viewDiffMessage = chatItems[5] + + assert.deepStrictEqual(viewDiffMessage.type, 'answer') + assert.deepStrictEqual( + viewDiffMessage.body, + 'Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.' + ) + }) + }) + + describe('Accept code', async () => { + it('Clicks on accept', async () => { + await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) + tab.clickButton(FollowUpTypes.AcceptCode) + await tab.waitForChatFinishesLoading() + + await waitForChatItems(7) + const acceptedMessage = tab.getChatItems()[7] + + assert.deepStrictEqual(acceptedMessage?.type, 'answer-part') + assert.deepStrictEqual(acceptedMessage?.followUp?.options?.[0].pillText, 'Accepted') + }) + }) + + describe('Reject code', async () => { + it('Clicks on reject', async () => { + await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) + tab.clickButton(FollowUpTypes.RejectCode) + await tab.waitForChatFinishesLoading() + + await waitForChatItems(7) + const rejectedMessage = tab.getChatItems()[7] + + assert.deepStrictEqual(rejectedMessage?.type, 'answer-part') + assert.deepStrictEqual(rejectedMessage?.followUp?.options?.[0].pillText, 'Rejected') + }) + }) + }) + } + }) +}) diff --git a/packages/amazonq/test/e2e/amazonq/welcome.test.ts b/packages/amazonq/test/e2e/amazonq/welcome.test.ts index 59ba7e728f2..d9f0ccd66bf 100644 --- a/packages/amazonq/test/e2e/amazonq/welcome.test.ts +++ b/packages/amazonq/test/e2e/amazonq/welcome.test.ts @@ -8,7 +8,8 @@ import { qTestingFramework } from './framework/framework' import sinon from 'sinon' import { Messenger } from './framework/messenger' import { MynahUIDataModel } from '@aws/mynah-ui' -import { assertQuickActions } from './assert' +import { FeatureContext } from 'aws-core-vscode/shared' +import { assertContextCommands, assertQuickActions } from './assert' describe('Amazon Q Welcome page', function () { let framework: qTestingFramework @@ -17,8 +18,15 @@ describe('Amazon Q Welcome page', function () { const availableCommands = ['/dev', '/test', '/review', '/doc', '/transform'] + const highlightCommand: FeatureContext = { + name: 'highlightCommand', + value: { + stringValue: '@highlight', + }, + variation: 'highlight command desc', + } beforeEach(() => { - framework = new qTestingFramework('welcome', true, [], 0) + framework = new qTestingFramework('welcome', true, [['highlightCommand', highlightCommand]], 0) tab = framework.getTabs()[0] // use the default tab that gets created store = tab.getStore() }) @@ -33,14 +41,8 @@ describe('Amazon Q Welcome page', function () { assertQuickActions(tab, availableCommands) }) - it('Shows @workspace', async () => { - assert.deepStrictEqual( - store.contextCommands - ?.map((x) => x.commands) - .flat() - .map((x) => x.command), - ['@workspace'] - ) + it('Shows context commands', async () => { + assertContextCommands(tab, ['@workspace', '@highlight']) }) describe('shows 3 times', async () => { diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts index a7a5d831f67..680caf4aff0 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts @@ -38,8 +38,9 @@ describe('session', () => { describe('preloader', () => { it('emits start chat telemetry', async () => { const session = await createSession({ messenger, conversationID, scheme: featureDevScheme }) + session.latestMessage = 'implement twosum in typescript' - await session.preloader('implement twosum in typescript') + await session.preloader() assertTelemetry('amazonq_startConversationInvoke', { amazonqConversationId: conversationID, diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts index e8ecefc171e..754558fd9cf 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts @@ -10,55 +10,111 @@ import { ContentLengthError, maxRepoSizeBytes, } from 'aws-core-vscode/amazonqFeatureDev' -import { assertTelemetry, createTestWorkspace } from 'aws-core-vscode/test' -import { fs, AmazonqCreateUpload } from 'aws-core-vscode/shared' -import { Span } from 'aws-core-vscode/telemetry' +import { assertTelemetry, getWorkspaceFolder, TestFolder } from 'aws-core-vscode/test' +import { fs, AmazonqCreateUpload, ZipStream } from 'aws-core-vscode/shared' +import { MetricName, Span } from 'aws-core-vscode/telemetry' import sinon from 'sinon' +import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' + +const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { + const files: Record = { + 'file.md': 'test content', + // only include when execution is enabled + 'devfile.yaml': 'test', + // .git folder is always dropped (because of vscode global exclude rules) + '.git/ref': '####', + // .gitignore should always be included + '.gitignore': 'node_models/*', + // non code files only when dev execution is enabled + 'abc.jar': 'jar-content', + 'data/logo.ico': 'binary-content', + } + const folder = await TestFolder.create() + + for (const [fileName, content] of Object.entries(files)) { + await folder.write(fileName, content) + } + + const expectedFiles = !devfileEnabled + ? ['file.md', '.gitignore'] + : ['devfile.yaml', 'file.md', '.gitignore', 'abc.jar', 'data/logo.ico'] + + const workspace = getWorkspaceFolder(folder.path) + sinon + .stub(CodeWhispererSettings.instance, 'getAutoBuildSetting') + .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) + + await testPrepareRepoData(workspace, expectedFiles) +} + +const testPrepareRepoData = async ( + workspace: vscode.WorkspaceFolder, + expectedFiles: string[], + expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }> +) => { + expectedFiles.sort((a, b) => a.localeCompare(b)) + const telemetry = new TelemetryHelper() + const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { + record: () => {}, + } as unknown as Span) + + assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) + // checksum is not the same across different test executions because some unique random folder names are generated + assert.strictEqual(result.zipFileChecksum.length, 44) + + if (expectedTelemetryMetrics) { + for (const metric of expectedTelemetryMetrics) { + assertTelemetry(metric.metricName, metric.value) + } + } + + // Unzip the buffer and compare the entry names + const zipEntries = await ZipStream.unzip(result.zipFileBuffer) + const actualZipEntries = zipEntries.map((entry) => entry.filename) + actualZipEntries.sort((a, b) => a.localeCompare(b)) + assert.deepStrictEqual(actualZipEntries, expectedFiles) +} describe('file utils', () => { describe('prepareRepoData', function () { - it('returns files in the workspace as a zip', async function () { - // these variables are a manual selection of settings for the test in order to test the collectFiles function - const fileAmount = 2 - const fileNamePrefix = 'file' - const fileNameSuffix = '.md' - const fileContent = 'test content' + afterEach(() => { + sinon.restore() + }) - const workspace = await createTestWorkspace(fileAmount, { fileNamePrefix, fileContent, fileNameSuffix }) + it('returns files in the workspace as a zip', async function () { + const folder = await TestFolder.create() + await folder.write('file1.md', 'test content') + await folder.write('file2.md', 'test content') + const workspace = getWorkspaceFolder(folder.path) - const telemetry = new TelemetryHelper() - const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { - record: () => {}, - } as unknown as Span) - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.strictEqual(telemetry.repositorySize, 24) + await testPrepareRepoData(workspace, ['file1.md', 'file2.md']) }) it('prepareRepoData ignores denied file extensions', async function () { - // these variables are a manual selection of settings for the test in order to test the collectFiles function - const fileAmount = 1 - const fileNamePrefix = 'file' - const fileNameSuffix = '.mp4' - const fileContent = 'test content' + const folder = await TestFolder.create() + await folder.write('file.mp4', 'test content') + const workspace = getWorkspaceFolder(folder.path) - const workspace = await createTestWorkspace(fileAmount, { fileNamePrefix, fileContent, fileNameSuffix }) - const telemetry = new TelemetryHelper() - const result = await prepareRepoData([workspace.uri.fsPath], [workspace], telemetry, { - record: () => {}, - } as unknown as Span) - - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.strictEqual(telemetry.repositorySize, 0) - assertTelemetry('amazonq_bundleExtensionIgnored', { filenameExt: 'mp4', count: 1 }) + await testPrepareRepoData( + workspace, + [], + [{ metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }] + ) + }) + + it('should ignore devfile.yaml when setting is disabled', async function () { + await testDevfilePrepareRepo(false) + }) + + it('should include devfile.yaml when setting is enabled', async function () { + await testDevfilePrepareRepo(true) }) // Test the logic that allows the customer to modify root source folder it('prepareRepoData throws a ContentLengthError code when repo is too big', async function () { - const workspace = await createTestWorkspace(1, {}) + const folder = await TestFolder.create() + await folder.write('file.md', 'test content') + const workspace = getWorkspaceFolder(folder.path) const telemetry = new TelemetryHelper() sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts index 5af3252ec82..a94bbd5a3fe 100644 --- a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts @@ -9,19 +9,21 @@ import * as sinon from 'sinon' import { onAcceptance, AcceptedSuggestionEntry, - session, + CodeWhispererSessionState, CodeWhispererTracker, RecommendationHandler, AuthUtil, + CodeWhispererSession, } from 'aws-core-vscode/codewhisperer' import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' import { assertTelemetryCurried } from 'aws-core-vscode/test' describe('onAcceptance', function () { + let session: CodeWhispererSession describe('onAcceptance', function () { beforeEach(async function () { + session = CodeWhispererSessionState.instance.getSession() await resetCodeWhispererGlobalVariables() - session.reset() }) afterEach(function () { diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts index ae02e7bd7c3..6cff08c2ded 100644 --- a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts @@ -8,15 +8,22 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' import { assertTelemetryCurried } from 'aws-core-vscode/test' -import { onInlineAcceptance, RecommendationHandler, AuthUtil, session } from 'aws-core-vscode/codewhisperer' +import { + onInlineAcceptance, + RecommendationHandler, + AuthUtil, + CodeWhispererSessionState, + CodeWhispererSession, +} from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' import { extensionVersion } from 'aws-core-vscode/shared' describe('onInlineAcceptance', function () { + let session: CodeWhispererSession describe('onInlineAcceptance', function () { beforeEach(async function () { + session = CodeWhispererSessionState.instance.getSession() await resetCodeWhispererGlobalVariables() - session.reset() }) afterEach(function () { diff --git a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts index ae7114a22c8..7b0888521f4 100644 --- a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts @@ -4,7 +4,12 @@ */ import assert from 'assert' import sinon from 'sinon' -import { SecurityIssueFilters, SecurityTreeViewFilterState } from 'aws-core-vscode/codewhisperer' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + SecurityIssueFilters, + SecurityTreeViewFilterState, +} from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' describe('model', function () { @@ -70,4 +75,100 @@ describe('model', function () { assert.deepStrictEqual(hiddenSeverities, ['High', 'Low']) }) }) + + describe('CodeIssueGroupingStrategyState', function () { + let sandbox: sinon.SinonSandbox + let state: CodeIssueGroupingStrategyState + + beforeEach(function () { + sandbox = sinon.createSandbox() + state = CodeIssueGroupingStrategyState.instance + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('instance', function () { + it('should return the same instance when called multiple times', function () { + const instance1 = CodeIssueGroupingStrategyState.instance + const instance2 = CodeIssueGroupingStrategyState.instance + assert.strictEqual(instance1, instance2) + }) + }) + + describe('getState', function () { + it('should return fallback when no state is stored', function () { + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + + it('should return stored state when valid', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + await state.setState(validStrategy) + + const result = state.getState() + + assert.equal(result, validStrategy) + }) + + it('should return fallback when stored state is invalid', async function () { + const invalidStrategy = 'invalid' + await state.setState(invalidStrategy) + + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('setState', function () { + it('should update state and fire change event for valid strategy', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(validStrategy) + + sinon.assert.calledWith(eventSpy, validStrategy) + }) + + it('should use fallback and fire change event for invalid strategy', async function () { + const invalidStrategy = 'invalid' + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(invalidStrategy) + + sinon.assert.calledWith(eventSpy, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('reset', function () { + it('should set state to fallback value', async function () { + const setStateStub = sandbox.stub(state, 'setState').resolves() + + await state.reset() + + sinon.assert.calledWith(setStateStub, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('onDidChangeState', function () { + it('should allow subscribing to state changes', async function () { + const listener = sandbox.spy() + const disposable = state.onDidChangeState(listener) + + await state.setState(CodeIssueGroupingStrategy.Severity) + + sinon.assert.calledWith(listener, CodeIssueGroupingStrategy.Severity) + disposable.dispose() + }) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts index 956999d64ad..a7f87a346ad 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/completionProvider.test.ts @@ -12,7 +12,7 @@ import { getLabel, Recommendation, RecommendationHandler, - session, + CodeWhispererSessionState, } from 'aws-core-vscode/codewhisperer' import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' @@ -39,6 +39,7 @@ describe('completionProviderService', function () { describe('getCompletionItem', function () { it('should return targetCompletionItem given input', function () { + const session = CodeWhispererSessionState.instance.getSession() session.startPos = new vscode.Position(0, 0) RecommendationHandler.instance.requestId = 'mock_requestId_getCompletionItem' session.sessionId = 'mock_sessionId_getCompletionItem' @@ -95,6 +96,7 @@ describe('completionProviderService', function () { describe('getCompletionItems', function () { it('should return completion items for each non-empty recommendation', async function () { + const session = CodeWhispererSessionState.instance.getSession() session.recommendations = [ { content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '\nvar a = 10' }, @@ -106,6 +108,7 @@ describe('completionProviderService', function () { }) it('should return empty completion items when recommendation is empty', async function () { + const session = CodeWhispererSessionState.instance.getSession() session.recommendations = [] const mockPosition = new vscode.Position(14, 83) const mockDocument = createMockDocument() diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts index 18fd7d2f21b..f1618c83dac 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts @@ -14,7 +14,7 @@ import { CodeSuggestionsState, ConfigurationEntry, CWInlineCompletionItemProvider, - session, + CodeWhispererSessionState, AuthUtil, listCodeWhispererCommandsId, DefaultCodeWhispererClient, @@ -46,6 +46,7 @@ describe('inlineCompletionService', function () { }) it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { + const session = CodeWhispererSessionState.instance.getSession() const mockEditor = createMockTextEditor() sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ result: 'Succeeded', @@ -70,6 +71,7 @@ describe('inlineCompletionService', function () { describe('clearInlineCompletionStates', function () { it('should remove inline reference and recommendations', async function () { + const session = CodeWhispererSessionState.instance.getSession() const fakeReferences = [ { message: '', diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts index 4bc10329f81..9668fa4c5a4 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' import { ReferenceInlineProvider, - session, + CodeWhispererSessionState, AuthUtil, DefaultCodeWhispererClient, RecommendationsList, @@ -55,6 +55,7 @@ describe('recommendationHandler', function () { }) it('should assign correct recommendations given input', async function () { + const session = CodeWhispererSessionState.instance.getSession() assert.strictEqual(CodeWhispererCodeCoverageTracker.instances.size, 0) assert.strictEqual( CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, @@ -74,7 +75,7 @@ describe('recommendationHandler', function () { } const handler = new RecommendationHandler() sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, session, 'Enter', false) const actual = session.recommendations const expected: RecommendationsList = [{ content: "print('Hello World!')" }, { content: '' }] assert.deepStrictEqual(actual, expected) @@ -85,6 +86,7 @@ describe('recommendationHandler', function () { }) it('should assign request id correctly', async function () { + const session = CodeWhispererSessionState.instance.getSession() const mockServerResult = { recommendations: [{ content: "print('Hello World!')" }, { content: '' }], $response: { @@ -99,7 +101,7 @@ describe('recommendationHandler', function () { const handler = new RecommendationHandler() sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) sinon.stub(handler, 'isCancellationRequested').returns(false) - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, session, 'Enter', false) assert.strictEqual(handler.requestId, 'test_request') assert.strictEqual(session.sessionId, 'test_request') assert.strictEqual(session.triggerType, 'AutoTrigger') @@ -125,12 +127,13 @@ describe('recommendationHandler', function () { supplementalContextItems: [], contentsLength: 100, latency: 0, - strategy: 'Empty', + strategy: 'empty', }) sinon.stub(performance, 'now').returns(0.0) + const session = CodeWhispererSessionState.instance.getSession() session.startPos = new vscode.Position(1, 0) session.startCursorOffset = 2 - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, session, 'Enter') const assertTelemetry = assertTelemetryCurried('codewhisperer_serviceInvocation') assertTelemetry({ codewhispererRequestId: 'test_request', @@ -167,10 +170,11 @@ describe('recommendationHandler', function () { const handler = new RecommendationHandler() sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) sinon.stub(performance, 'now').returns(0.0) + const session = CodeWhispererSessionState.instance.getSession() session.startPos = new vscode.Position(1, 0) session.requestIdList = ['test_request_empty'] session.startCursorOffset = 2 - await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, session, 'Enter') const assertTelemetry = assertTelemetryCurried('codewhisperer_userDecision') assertTelemetry({ codewhispererRequestId: 'test_request_empty', @@ -192,6 +196,7 @@ describe('recommendationHandler', function () { sinon.restore() }) it('should return true if any response is not empty', function () { + const session = CodeWhispererSessionState.instance.getSession() const handler = new RecommendationHandler() session.recommendations = [ { @@ -204,12 +209,14 @@ describe('recommendationHandler', function () { }) it('should return false if response is empty', function () { + const session = CodeWhispererSessionState.instance.getSession() const handler = new RecommendationHandler() session.recommendations = [] assert.ok(!handler.isValidResponse()) }) it('should return false if all response has no string length', function () { + const session = CodeWhispererSessionState.instance.getSession() const handler = new RecommendationHandler() session.recommendations = [{ content: '' }, { content: '' }] assert.ok(!handler.isValidResponse()) @@ -222,6 +229,7 @@ describe('recommendationHandler', function () { }) it('should set the completion type to block given a multi-line suggestion', function () { + const session = CodeWhispererSessionState.instance.getSession() session.setCompletionType(0, { content: 'test\n\n \t\r\nanother test' }) assert.strictEqual(session.getCompletionType(0), 'Block') @@ -233,6 +241,7 @@ describe('recommendationHandler', function () { }) it('should set the completion type to line given a single-line suggestion', function () { + const session = CodeWhispererSessionState.instance.getSession() session.setCompletionType(0, { content: 'test' }) assert.strictEqual(session.getCompletionType(0), 'Line') @@ -241,6 +250,7 @@ describe('recommendationHandler', function () { }) it('should set the completion type to line given a multi-line completion but only one-lien of non-blank sequence', function () { + const session = CodeWhispererSessionState.instance.getSession() session.setCompletionType(0, { content: 'test\n\t' }) assert.strictEqual(session.getCompletionType(0), 'Line') @@ -257,6 +267,7 @@ describe('recommendationHandler', function () { describe('on event change', async function () { beforeEach(function () { + const session = CodeWhispererSessionState.instance.getSession() const fakeReferences = [ { message: '', @@ -274,12 +285,14 @@ describe('recommendationHandler', function () { }) it('should remove inline reference onEditorChange', async function () { + const session = CodeWhispererSessionState.instance.getSession() session.sessionId = 'aSessionId' RecommendationHandler.instance.requestId = 'aRequestId' await RecommendationHandler.instance.onEditorChange() assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) }) it('should remove inline reference onFocusChange', async function () { + const session = CodeWhispererSessionState.instance.getSession() session.sessionId = 'aSessionId' RecommendationHandler.instance.requestId = 'aRequestId' await RecommendationHandler.instance.onFocusChange() diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts index bd7c3aab8de..4d973735c9f 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts @@ -10,17 +10,24 @@ import { SecurityTreeViewFilterState, SecurityIssueProvider, SeverityItem, + CodeIssueGroupingStrategyState, + CodeIssueGroupingStrategy, } from 'aws-core-vscode/codewhisperer' import { createCodeScanIssue } from 'aws-core-vscode/test' import assert from 'assert' import sinon from 'sinon' +import path from 'path' describe('SecurityIssueTreeViewProvider', function () { - let securityIssueProvider: SecurityIssueProvider let securityIssueTreeViewProvider: SecurityIssueTreeViewProvider beforeEach(function () { - securityIssueProvider = SecurityIssueProvider.instance + SecurityIssueProvider.instance.issues = [ + { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + ] securityIssueTreeViewProvider = new SecurityIssueTreeViewProvider() }) @@ -44,13 +51,6 @@ describe('SecurityIssueTreeViewProvider', function () { describe('getChildren', function () { it('should return sorted list of severities if element is undefined', function () { - securityIssueProvider.issues = [ - { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - ] - const element = undefined const result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[] assert.strictEqual(result.length, 5) @@ -102,5 +102,55 @@ describe('SecurityIssueTreeViewProvider', function () { const result = securityIssueTreeViewProvider.getChildren(element) as IssueItem[] assert.strictEqual(result.length, 0) }) + + it('should return severity-grouped items when grouping strategy is Severity', function () { + sinon.stub(CodeIssueGroupingStrategyState.instance, 'getState').returns(CodeIssueGroupingStrategy.Severity) + + const severityItems = securityIssueTreeViewProvider.getChildren() as SeverityItem[] + for (const [index, [severity, expectedIssueCount]] of [ + ['Critical', 0], + ['High', 8], + ['Medium', 0], + ['Low', 0], + ['Info', 0], + ].entries()) { + const currentSeverityItem = severityItems[index] + assert.strictEqual(currentSeverityItem.label, severity) + assert.strictEqual(currentSeverityItem.issues.length, expectedIssueCount) + + const issueItems = securityIssueTreeViewProvider.getChildren(currentSeverityItem) as IssueItem[] + assert.ok(issueItems.every((item) => item.iconPath === undefined)) + assert.ok( + issueItems.every((item) => item.description?.toString().startsWith(path.basename(item.filePath))) + ) + } + }) + + it('should return file-grouped items when grouping strategy is FileLocation', function () { + sinon + .stub(CodeIssueGroupingStrategyState.instance, 'getState') + .returns(CodeIssueGroupingStrategy.FileLocation) + + const result = securityIssueTreeViewProvider.getChildren() as FileItem[] + for (const [index, [fileName, expectedIssueCount]] of [ + ['a', 2], + ['b', 2], + ['c', 2], + ['d', 2], + ].entries()) { + const currentFileItem = result[index] + assert.strictEqual(currentFileItem.label, fileName) + assert.strictEqual(currentFileItem.issues.length, expectedIssueCount) + assert.strictEqual(currentFileItem.description, 'file/path') + + const issueItems = securityIssueTreeViewProvider.getChildren(currentFileItem) as IssueItem[] + assert.ok( + issueItems.every((item) => + item.iconPath?.toString().includes(`${item.issue.severity.toLowerCase()}.svg`) + ) + ) + assert.ok(issueItems.every((item) => item.description?.toString().startsWith('[Ln '))) + } + }) }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts index b0086b2a205..b167721f334 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts @@ -13,7 +13,10 @@ import { mapToAggregatedList, DefaultCodeWhispererClient, ListCodeScanFindingsResponse, + pollScanJobStatus, + SecurityScanTimedOutError, } from 'aws-core-vscode/codewhisperer' +import { timeoutUtils } from 'aws-core-vscode/shared' import assert from 'assert' import sinon from 'sinon' import * as vscode from 'vscode' @@ -50,7 +53,7 @@ const buildMockListCodeScanFindingsResponse = ( ): Awaited>> => ({ $response: { hasNextPage: () => false, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null data: undefined, error: undefined, requestId: '', @@ -128,6 +131,31 @@ describe('securityScanHandler', function () { assert.equal(aggregatedCodeScanIssueList.length, 2) assert.equal(aggregatedCodeScanIssueList[0].issues.length, 3) }) + + it('should set autoDetected based on scope', async function () { + mockClient.listCodeScanFindings.resolves( + buildMockListCodeScanFindingsResponse(JSON.stringify([buildRawCodeScanIssue()])) + ) + for (const [scope, expectedValue] of [ + [CodeAnalysisScope.FILE_AUTO, true], + [CodeAnalysisScope.FILE_ON_DEMAND, false], + [CodeAnalysisScope.PROJECT, false], + ] as [CodeAnalysisScope, boolean][]) { + const aggregatedCodeScanIssueList = await listScanResults( + mockClient, + 'jobId', + 'codeScanFindingsSchema', + ['projectPath'], + scope, + undefined + ) + assert.ok( + aggregatedCodeScanIssueList.every((item) => + item.issues.every((issue) => issue.autoDetected === expectedValue) + ) + ) + } + }) }) describe('mapToAggregatedList', () => { @@ -239,4 +267,57 @@ describe('securityScanHandler', function () { assert.strictEqual(codeScanIssueMap.get('file1.ts')?.length, 1) }) }) + + describe('pollScanJobStatus', function () { + let mockClient: Stub + let clock: sinon.SinonFakeTimers + const mockJobId = 'test-job-id' + const mockStartTime = Date.now() + + beforeEach(function () { + mockClient = stub(DefaultCodeWhispererClient) + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }) + sinon.stub(timeoutUtils, 'sleep').resolves() + }) + + afterEach(function () { + sinon.restore() + clock.restore() + }) + + it('should return status when scan completes successfully', async function () { + mockClient.getCodeScan + .onFirstCall() + .resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + .onSecondCall() + .resolves({ status: 'Completed', $response: { requestId: 'req2' } }) + + const result = await pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.FILE_AUTO, mockStartTime) + assert.strictEqual(result, 'Completed') + }) + + it('should throw SecurityScanTimedOutError when polling exceeds timeout for express scans', async function () { + mockClient.getCodeScan.resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + + const pollPromise = pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.FILE_AUTO, mockStartTime) + + const expectedTimeoutMs = 60_000 + clock.tick(expectedTimeoutMs + 1000) + + await assert.rejects(() => pollPromise, SecurityScanTimedOutError) + }) + + it('should throw SecurityScanTimedOutError when polling exceeds timeout for standard scans', async function () { + mockClient.getCodeScan.resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + + const pollPromise = pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.PROJECT, mockStartTime) + + const expectedTimeoutMs = 600_000 + clock.tick(expectedTimeoutMs + 1000) + + await assert.rejects(() => pollPromise, SecurityScanTimedOutError) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts index 0f1429f130b..797de801428 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/telemetry.test.ts @@ -22,7 +22,7 @@ import { invokeRecommendation, ConfigurationEntry, RecommendationHandler, - session, + CodeWhispererSessionState, vsCodeCursorUpdateDelay, AuthUtil, } from 'aws-core-vscode/codewhisperer' @@ -36,6 +36,7 @@ type CodeWhispererResponse = ListRecommendationsResponse & { let tempFolder: string describe.skip('CodeWhisperer telemetry', async function () { + const session = CodeWhispererSessionState.instance.getSession() let sandbox: sinon.SinonSandbox let client: DefaultCodeWhispererClient @@ -519,6 +520,7 @@ async function manualTrigger( // Note: RecommendationHandler.isSuggestionVisible seems not to work well, hence not using it async function waitUntilSuggestionSeen(index: number = 0) { + const session = CodeWhispererSessionState.instance.getSession() const state = await waitUntil( async () => { const r = session.getSuggestionState(index) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts new file mode 100644 index 00000000000..1d9b878133f --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { UserWrittenCodeTracker, TelemetryHelper, AuthUtil } from 'aws-core-vscode/codewhisperer' +import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' + +describe('userWrittenCodeTracker', function () { + describe('isActive()', function () { + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + UserWrittenCodeTracker.instance.reset() + sinon.restore() + }) + + it('inactive case: telemetryEnable = true, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false) + }) + + it('inactive case: telemetryEnabled = false, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false) + }) + + it('active case: telemetryEnabled = true, isConnected = true', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), true) + }) + }) + + describe('onDocumentChange', function () { + let tracker: UserWrittenCodeTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = UserWrittenCodeTracker.instance + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + UserWrittenCodeTracker.instance.reset() + }) + + it('Should skip when content change size is more than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 600), + rangeOffset: 0, + rangeLength: 600, + text: 'def twoSum(nums, target):\nfor '.repeat(20), + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 0) + assert.strictEqual(tracker.getUserWrittenLines('python'), 0) + }) + + it('Should not skip when content change size is less than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 49), + rangeOffset: 0, + rangeLength: 49, + text: 'a = 123'.repeat(7), + }, + ], + }) + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument('', 'test.java', 'java'), + contentChanges: [ + { + range: new vscode.Range(0, 0, 1, 3), + rangeOffset: 0, + rangeLength: 11, + text: 'a = 123\nbcd', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 49) + assert.strictEqual(tracker.getUserWrittenLines('python'), 0) + assert.strictEqual(tracker.getUserWrittenCharacters('java'), 11) + assert.strictEqual(tracker.getUserWrittenLines('java'), 1) + assert.strictEqual(tracker.getUserWrittenLines('cpp'), 0) + }) + + it('Should skip when Q is editing', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onQStartsMakingEdits() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 30), + rangeOffset: 0, + rangeLength: 30, + text: 'def twoSum(nums, target):\nfor', + }, + ], + }) + tracker.onQFinishesEdits() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 2), + rangeOffset: 0, + rangeLength: 2, + text: '\na', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + assert.strictEqual(tracker.getUserWrittenLines('python'), 1) + }) + + it('Should not reduce tokens when delete', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'b', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 1, + rangeLength: 1, + text: '', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts new file mode 100644 index 00000000000..9c5e00cd6f7 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts @@ -0,0 +1,45 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { createQuickPickPrompterTester, QuickPickPrompterTester } from 'aws-core-vscode/test' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + createCodeIssueGroupingStrategyPrompter, +} from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import assert from 'assert' +import vscode from 'vscode' + +const severity = { data: CodeIssueGroupingStrategy.Severity, label: 'Severity' } +const fileLocation = { data: CodeIssueGroupingStrategy.FileLocation, label: 'File Location' } + +describe('createCodeIssueGroupingStrategyPrompter', function () { + let tester: QuickPickPrompterTester + + beforeEach(function () { + tester = createQuickPickPrompterTester(createCodeIssueGroupingStrategyPrompter()) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should list grouping strategies', async function () { + tester.assertItems([severity, fileLocation]) + tester.hide() + await tester.result() + }) + + it('should update state on selection', async function () { + const originalState = CodeIssueGroupingStrategyState.instance.getState() + assert.equal(originalState, CodeIssueGroupingStrategy.Severity) + + tester.selectItems(fileLocation) + tester.addCallback(() => vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem')) + + await tester.result() + assert.equal(CodeIssueGroupingStrategyState.instance.getState(), fileLocation.data) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts index 8f54a43bf52..91e26e36111 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts @@ -8,7 +8,12 @@ import * as FakeTimers from '@sinonjs/fake-timers' import * as vscode from 'vscode' import * as sinon from 'sinon' import * as crossFile from 'aws-core-vscode/codewhisperer' -import { aStringWithLineCount, createMockTextEditor, installFakeClock } from 'aws-core-vscode/test' +import { + aLongStringWithLineCount, + aStringWithLineCount, + createMockTextEditor, + installFakeClock, +} from 'aws-core-vscode/test' import { FeatureConfigProvider, crossFileContextConfig } from 'aws-core-vscode/codewhisperer' import { assertTabCount, @@ -71,8 +76,8 @@ describe('crossFileContextUtil', function () { assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) }) - it('for t1 group, should return repomap + opentabs context', async function () { - await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + it('for t1 group, should return repomap + opentabs context, should not exceed 20k total length', async function () { + await toTextEditor(aLongStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { preview: false, }) @@ -85,7 +90,7 @@ describe('crossFileContextUtil', function () { .withArgs(sinon.match.any, sinon.match.any, 'codemap') .resolves([ { - content: 'foo', + content: 'foo'.repeat(3000), score: 0, filePath: 'q-inline', }, @@ -93,17 +98,15 @@ describe('crossFileContextUtil', function () { const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) assert.ok(actual) - assert.strictEqual(actual.supplementalContextItems.length, 4) + assert.strictEqual(actual.supplementalContextItems.length, 3) assert.strictEqual(actual?.strategy, 'codemap') assert.deepEqual(actual?.supplementalContextItems[0], { - content: 'foo', + content: 'foo'.repeat(3000), score: 0, filePath: 'q-inline', }) - assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) - assert.strictEqual(actual.supplementalContextItems[3].content.split('\n').length, 50) }) it.skip('for t2 group, should return global bm25 context and no repomap', async function () { diff --git a/packages/amazonq/test/unit/codewhisperer/util/telemetryHelper.test.ts b/packages/amazonq/test/unit/codewhisperer/util/telemetryHelper.test.ts index e042b1d43a2..99f2585a285 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/telemetryHelper.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/telemetryHelper.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import { assertTelemetryCurried, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' -import { TelemetryHelper, Completion, session } from 'aws-core-vscode/codewhisperer' +import { TelemetryHelper, Completion, CodeWhispererSessionState } from 'aws-core-vscode/codewhisperer' import { CodewhispererCompletionType, CodewhispererSuggestionState, @@ -39,6 +39,7 @@ function aCompletion(): Completion { } describe('telemetryHelper', function () { + const session = CodeWhispererSessionState.instance.getSession() describe('clientComponentLatency', function () { let sut: TelemetryHelper @@ -48,6 +49,7 @@ describe('telemetryHelper', function () { afterEach(function () { sinon.restore() + session.reset() }) it('resetClientComponentLatencyTime should reset state variables', function () { diff --git a/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts index 4729d65d416..230e950b045 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/zipUtil.test.ts @@ -195,22 +195,5 @@ describe('zipUtil', function () { await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Zip failed/) }) - - it('Should handle file copy to downloads folder error', async function () { - // Mock LSP client - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - - const mkdirSpy = sinon.spy(fs, 'mkdir') - sinon.stub(fs, 'exists').resolves(true) - sinon.stub(fs, 'copy').rejects(new Error('Copy failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Copy failed/) - - // Verify mkdir was called for all directories - assert(mkdirSpy.called, 'mkdir should have been called') - assert.strictEqual(mkdirSpy.callCount, 4, 'mkdir should have been called 4 times') - }) }) }) diff --git a/packages/core/package.json b/packages/core/package.json index 5f5e59bfa13..d9f19bad5f7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -499,6 +499,8 @@ "@aws-sdk/client-cloudformation": "^3.667.0", "@aws-sdk/client-cloudwatch-logs": "^3.666.0", "@aws-sdk/client-cognito-identity": "^3.637.0", + "@aws-sdk/client-docdb": "^3.699.0", + "@aws-sdk/client-docdb-elastic": "^3.699.0", "@aws-sdk/client-lambda": "^3.637.0", "@aws-sdk/client-sso": "^3.342.0", "@aws-sdk/client-sso-oidc": "^3.574.0", @@ -508,7 +510,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "^4.21.4", + "@aws/mynah-ui": "^4.22.1", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^2.3.1", @@ -519,9 +521,9 @@ "@vscode/debugprotocol": "^1.57.0", "@zip.js/zip.js": "^2.7.41", "adm-zip": "^0.5.10", - "amazon-states-language-service": "^1.13.0", + "amazon-states-language-service": "^1.15.0", "async-lock": "^1.4.0", - "aws-sdk": "^2.1384.0", + "aws-sdk": "^2.1692.0", "aws-ssm-document-language-service": "^1.0.0", "bytes": "^3.1.2", "cross-fetch": "^4.0.0", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index d0e31cbcb33..850f7242b45 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -20,7 +20,8 @@ "AWS.configuration.description.suppressPrompts": "Prompts which ask for confirmation. Checking an item suppresses the prompt.", "AWS.configuration.enableCodeLenses": "Enable SAM hints in source code and template.yaml files", "AWS.configuration.description.resources.enabledResources": "AWS resources to display in the 'Resources' portion of the explorer.", - "AWS.configuration.description.experiments": "Try experimental features and give feedback. Note that experimental features may be removed at any time.\n * `jsonResourceModification` - Enables basic create, update, and delete support for cloud resources via the JSON Resources explorer component.\n * `ec2RemoteConnect` - Allows interfacing with EC2 instances with options to start, stop, and establish remote connections. Remote connections are done over SSM and can be through a terminal or a remote VSCode window.", + "AWS.configuration.description.featureDevelopment.allowRunningCodeAndTests": "Allow /dev to run code and test commands", + "AWS.configuration.description.experiments": "Try experimental features and give feedback. Note that experimental features may be removed at any time.\n * `jsonResourceModification` - Enables basic create, update, and delete support for cloud resources via the JSON Resources explorer component.", "AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files", "AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", "AWS.configuration.description.awssam.debug.api": "API Gateway configuration", @@ -74,7 +75,7 @@ "AWS.configuration.description.amazonq": "Amazon Q creates a code reference when you insert a code suggestion from Amazon Q that is similar to training data. When unchecked, Amazon Q will not show code suggestions that have code references. If you authenticate through IAM Identity Center, this setting is controlled by your Amazon Q administrator. [Learn More](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-reference.html)", "AWS.configuration.description.amazonq.shareContentWithAWS": "When checked, your content processed by Amazon Q may be used for service improvement (except for content processed for users with the Amazon Q Developer Pro Tier). Unchecking this box will cause AWS to delete any of your content used for that purpose. The information used to provide the Amazon Q service to you will not be affected. See the [Service Terms](https://aws.amazon.com/service-terms) for more details.", "AWS.configuration.description.amazonq.importRecommendation": "Amazon Q will add import statements with inline code suggestions when necessary.", - "AWS.configuration.description.amazonq.workspaceIndex": "When you add @workspace to your question in Amazon Q chat, Amazon Q will index your open workspace files locally to use as context for its response. Extra CPU usage is expected while indexing a workspace. This will not impact Amazon Q features or your IDE, but you may manage CPU usage by setting the number of local threads in 'Local Workspace Index Threads'.", + "AWS.configuration.description.amazonq.workspaceIndex": "When you add @workspace to your question in Amazon Q chat, Amazon Q will index your workspace files locally to use as context for its response. Extra CPU usage is expected while indexing a workspace. This will not impact Amazon Q features or your IDE, but you may manage CPU usage by setting the number of local threads in 'Local Workspace Index Threads'.", "AWS.configuration.description.amazonq.workspaceIndexWorkerThreads": "Number of worker threads of Amazon Q local index process. '0' will use the system default worker threads for balance performance. You may increase this number to more quickly index your workspace, but only up to your hardware's number of CPU cores. Please restart VS Code or reload the VS Code window after changing worker threads.", "AWS.configuration.description.amazonq.workspaceIndexUseGPU": "Enable GPU to help index your local workspace files. Only applies to Linux and Windows.", "AWS.configuration.description.amazonq.workspaceIndexMaxSize": "The maximum size of local workspace files to be indexed in MB", @@ -91,6 +92,7 @@ "AWS.command.appBuilder.deploy": "Deploy SAM Application", "AWS.command.appBuilder.build": "Build SAM Template", "AWS.command.appBuilder.searchLogs": "Search Logs", + "AWS.command.appBuilder.tailLogs": "Tail Logs", "AWS.command.refreshappBuilderExplorer": "Refresh Application Builder Explorer", "AWS.command.applicationComposer.openDialog": "Open Template with Infrastructure Composer...", "AWS.command.auth.addConnection": "Add New Connection", @@ -133,6 +135,7 @@ "AWS.command.amazonq.acceptFix": "Accept Fix", "AWS.command.amazonq.regenerateFix": "Regenerate Fix", "AWS.command.amazonq.filterIssues": "Filter Issues", + "AWS.command.amazonq.groupIssues": "Group Issues", "AWS.command.deploySamApplication": "Deploy SAM Application", "AWS.command.aboutToolkit": "About", "AWS.command.downloadLambda": "Download...", @@ -230,6 +233,17 @@ "AWS.command.apprunner.copyServiceUrl": "Copy Service URL", "AWS.command.apprunner.open": "Open in Browser", "AWS.command.apprunner.startDeployment": "Start Deployment", + "AWS.command.docdb.createCluster": "Create DocumentDB Cluster", + "AWS.command.docdb.createInstance": "Add Instance...", + "AWS.command.docdb.modifyInstance": "Modify Class...", + "AWS.command.docdb.rebootInstance": "Reboot Instance", + "AWS.command.docdb.startCluster": "Start Cluster", + "AWS.command.docdb.stopCluster": "Stop Cluster", + "AWS.command.docdb.tags": "Tags...", + "AWS.command.docdb.open": "Open in Browser", + "AWS.command.docdb.openHelp": "Open Getting Started Guide", + "AWS.command.docdb.copyEndpoint": "Copy Endpoint", + "AWS.command.docdb.addRegion": "Add region...", "AWS.command.resources.copyIdentifier": "Copy Identifier", "AWS.command.resources.configure": "Show Resources...", "AWS.command.codewhisperer.introduction": "What is Amazon Q?", @@ -239,9 +253,11 @@ "AWS.command.codewhisperer.openReferencePanel": "Open Code Reference Log", "AWS.command.q.transform.acceptChanges": "Accept", "AWS.command.q.transform.rejectChanges": "Reject", - "AWS.command.q.transform.showChanges": "Show Proposed Changes", - "AWS.command.q.transform.showChangeSummary": "Show Transformation Summary", - "AWS.command.q.transform.showTransformationPlan": "Show Transformation Plan", + "AWS.command.q.transform.stopJobInHub": "Stop job", + "AWS.command.q.transform.viewJobProgress": "View job progress", + "AWS.command.q.transform.viewJobStatus": "View job status", + "AWS.command.q.transform.showTransformationPlan": "View plan", + "AWS.command.q.transform.showChangeSummary": "View summary", "AWS.command.threatComposer.createNew": "Create New Threat Composer File", "AWS.command.threatComposer.newFile": "Threat Composer File", "AWS.threatComposer.page.title": "{0} (Threat Composer)", @@ -267,6 +283,7 @@ "AWS.generic.copyUrl": "Copy URL", "AWS.generic.promptDelete": "Delete...", "AWS.generic.promptUpdate": "Update...", + "AWS.generic.promptRename": "Rename...", "AWS.generic.preview": "Preview", "AWS.generic.viewDocs": "View Documentation", "AWS.generic.moreActions": "More Actions...", @@ -292,6 +309,7 @@ "AWS.codewhisperer.customization.notification.new_customizations.learn_more": "Learn More", "AWS.amazonq.title": "Amazon Q", "AWS.amazonq.chat": "Chat", + "AWS.amazonq.chat.workspacecontext.enable.message": "Amazon Q: Workspace index is now enabled. You can disable it from Amazon Q settings.", "AWS.amazonq.security": "Code Issues", "AWS.amazonq.login": "Login", "AWS.amazonq.learnMore": "Learn More About Amazon Q", @@ -309,6 +327,11 @@ "AWS.amazonq.scans.projectScanInProgress": "Workspace review is in progress...", "AWS.amazonq.scans.fileScanInProgress": "File review is in progress...", "AWS.amazonq.scans.noGitRepo": "Your workspace is not in a git repository. I'll review your project files for security issues, and your in-flight changes for code quality issues.", + "AWS.amazonq.codefix.error.monthlyLimitReached": "Maximum code fix count reached for this month.", + "AWS.amazonq.scans.severity": "Severity", + "AWS.amazonq.scans.fileLocation": "File Location", + "AWS.amazonq.scans.groupIssues": "Group Issues", + "AWS.amazonq.scans.groupIssues.placeholder": "Select how to group code issues", "AWS.amazonq.featureDev.error.conversationIdNotFoundError": "Conversation id must exist before starting code generation", "AWS.amazonq.featureDev.error.contentLengthError": "The folder you selected is too large for me to use as context. Please choose a smaller folder to work on. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.featureDev.error.illegalStateTransition": "Illegal transition between states, restart the conversation", @@ -352,12 +375,18 @@ "AWS.amazonq.featureDev.pillText.selectOption": "Choose an option to proceed", "AWS.amazonq.featureDev.pillText.unableGenerateChanges": "Unable to generate any file changes", "AWS.amazonq.featureDev.pillText.provideFeedback": "Provide feedback & regenerate", - "AWS.amazonq.featureDev.answer.generateSuggestion": "Would you like to generate a suggestion for this? You'll review a file diff before inserting into your project.", + "AWS.amazonq.featureDev.pillText.generateDevFile": "Generate devfile to build code", + "AWS.amazonq.featureDev.pillText.acceptForProject": "Yes, use my devfile for this project", + "AWS.amazonq.featureDev.pillText.declineForProject": "No, thanks", + "AWS.amazonq.featureDev.answer.generateSuggestion": "Would you like to generate a suggestion for this? You’ll review a file diff before inserting into your project.", "AWS.amazonq.featureDev.answer.qGeneratedCode": "The Amazon Q Developer Agent for software development has generated code for you to review", "AWS.amazonq.featureDev.answer.howCodeCanBeImproved": "How can I improve the code for your use case?", "AWS.amazonq.featureDev.answer.updateCode": "Okay, I updated your code files. Would you like to work on another task?", "AWS.amazonq.featureDev.answer.sessionClosed": "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow.", "AWS.amazonq.featureDev.answer.newTaskChanges": "What new task would you like to work on?", + "AWS.amazonq.featureDev.answer.devFileSuggestion": "For future tasks in this project, I can create a devfile to build and test code as I generate it. This can improve the quality of generated code. To allow me to create a devfile, choose **Generate devfile to build code**.", + "AWS.amazonq.featureDev.answer.settingUpdated": "I've updated your settings so I can run code and test commands based on your devfile for this project. You can update this setting under **Amazon Q: Allow Q /dev to run code and test commands**.", + "AWS.amazonq.featureDev.answer.devFileInRepository": "I noticed that your repository has a `devfile.yaml`. Would you like me to use the devfile to build and test your project as I generate code? \n\nFor more information on using devfiles to improve code generation, see the Amazon Q Developer documentation.", "AWS.amazonq.featureDev.placeholder.chatInputDisabled": "Chat input is disabled", "AWS.amazonq.featureDev.placeholder.additionalImprovements": "Describe your task or issue in detail", "AWS.amazonq.featureDev.placeholder.feedback": "Provide feedback or comments", @@ -370,6 +399,7 @@ "AWS.amazonq.doc.answer.readmeCreated": "I've created a README for your code.", "AWS.amazonq.doc.answer.readmeUpdated": "I've updated your README.", "AWS.amazonq.doc.answer.codeResult": "You can accept the changes to your files, or describe any additional changes you'd like me to make.", + "AWS.amazonq.doc.answer.acceptOrReject": "You can accept or reject the changes to your files.", "AWS.amazonq.doc.answer.scanning": "Scanning source files", "AWS.amazonq.doc.answer.summarizing": "Summarizing source files", "AWS.amazonq.doc.answer.generating": "Generating documentation", @@ -379,19 +409,23 @@ "AWS.amazonq.doc.error.noFolderSelected": "It looks like you didn't choose a folder. Choose a folder to continue.", "AWS.amazonq.doc.error.contentLengthError": "Your workspace is too large for me to review. Your workspace must be within the quota, even if you choose a smaller folder. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.readmeTooLarge": "The README in your folder is too large for me to review. Try reducing the size of your README, or choose a folder with a smaller README. For more information on quotas, see the Amazon Q Developer documentation.", - "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README is too large. Try reducing the size of your README, or asking for a smaller update. For more information on quotas, see the Amazon Q Developer documentation.", + "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README exceeds document size limits. Try reducing the size of your current README or working on a smaller task that won't produce as much content. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.workspaceEmpty": "The folder you chose did not contain any source files in a supported language. Choose another folder and try again. For more information on supported languages, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptTooVague": "I need more information to make changes to your README. Try providing some of the following details:\n- Which sections you want to modify\n- The content you want to add or remove\n- Specific issues that need correcting\n\nFor more information on prompt best practices, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptUnrelated": "These changes don't seem related to documentation. Try describing your changes again, using the following best practices:\n- Changes should relate to how project functionality is reflected in the README\n- Content you refer to should be available in your codebase\n\n For more information on prompt best practices, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.docGen.default": "I'm sorry, I ran into an issue while trying to generate your documentation. Please try again.", "AWS.amazonq.doc.error.noChangeRequiredException": "I couldn't find any code changes to update in the README. Try another documentation task.", - "AWS.amazonq.doc.error.promptRefusal": "I'm sorry, I can't generate documentation for this folder. Please make sure your message and code files comply with the Please make sure your message and code files comply with the AWS Responsible AI Policy.", + "AWS.amazonq.doc.error.promptRefusal": "I'm sorry, I can't generate documentation for this folder. Please make sure your message and code files comply with the AWS Responsible AI Policy.", "AWS.amazonq.doc.placeholder.editReadme": "Describe documentation changes", "AWS.amazonq.doc.pillText.closeSession": "End session", "AWS.amazonq.doc.pillText.newTask": "Start a new documentation task", "AWS.amazonq.doc.pillText.update": "Update README to reflect code", "AWS.amazonq.doc.pillText.makeChange": "Make a specific change", + "AWS.amazonq.doc.pillText.accept": "Accept", + "AWS.amazonq.doc.pillText.reject": "Reject", + "AWS.amazonq.doc.pillText.makeChanges": "Make changes", "AWS.amazonq.inline.invokeChat": "Inline chat", + "AWS.amazonq.opensettings:": "Open settings", "AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough", "AWS.toolkit.lambda.walkthrough.title": "Get started building your application", "AWS.toolkit.lambda.walkthrough.description": "Your quick guide to build an application visually, iterate locally, and deploy to the cloud!", diff --git a/packages/core/resources/css/base-cloud9.css b/packages/core/resources/css/base-cloud9.css deleted file mode 100644 index ec1e30468c6..00000000000 --- a/packages/core/resources/css/base-cloud9.css +++ /dev/null @@ -1,68 +0,0 @@ -/* TODO: remove this when Cloud9 injects the correct styling information into webviews */ -@import url('./base.css'); - -body { - /* Temporary variables for C9 to shade/tint elements. Best-effort styling based off current theme. */ - /* Since these are applied as rgba, it's very easy to make things look 'washed-out' or too dark */ - --tint: 255, 255, 255; - --shade: 0, 0, 0; -} - -input[type='text'][data-invalid='true'], -input[type='number'][data-invalid='true'] { - border: 1px solid var(--vscode-inputValidation-errorBorder); - border-bottom: 0; - background: none; -} - -/* "Cloud9 gray" in input boxes (not buttons/radios). */ -body.vscode-dark input:not([type='submit']):not([type='radio']) { - background-color: rgba(var(--shade), 0.1); -} - -input:disabled { - filter: none; -} - -body.vscode-dark select { - background: rgba(var(--shade), 0.1); -} - -body.vscode-dark .header { - background-color: rgba(var(--tint), 0.02); -} -body.vscode-light .header { - background-color: rgba(var(--shade), 0.02); -} - -body.vscode-dark .notification { - background-color: #2a2a2a; -} -body.vscode-light .notification { - background-color: #f7f7f7; - box-shadow: 2px 2px 8px #aaa; -} - -button:disabled { - filter: none; -} - -/* Text area */ -textarea { - background: none; -} -body.vscode-dark textarea { - background: rgba(var(--shade), 0.1); -} - -/* Overrides */ -body.vscode-dark .settings-panel { - background: rgba(var(--tint), 0.02) !important; -} -body.vscode-light .settings-panel { - background: rgba(var(--shade), 0.02) !important; -} - -.button-container h1 { - margin: 0px; -} diff --git a/packages/core/resources/css/securityIssue.css b/packages/core/resources/css/securityIssue.css index d766ccd8fe2..5cd64211ae8 100644 --- a/packages/core/resources/css/securityIssue.css +++ b/packages/core/resources/css/securityIssue.css @@ -524,6 +524,13 @@ pre.center { pre.error { color: var(--vscode-diffEditorOverview-removedForeground); + background-color: var(--vscode-diffEditor-removedTextBackground); + white-space: initial; +} + +a.cursor { + cursor: pointer; + text-decoration: none; } .dot-typing { diff --git a/packages/core/resources/icons/cloud9/dark/vscode-help.svg b/packages/core/resources/icons/cloud9/dark/vscode-help.svg deleted file mode 100644 index 94b17dfd8b4..00000000000 --- a/packages/core/resources/icons/cloud9/dark/vscode-help.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/core/resources/icons/cloud9/light/vscode-help.svg b/packages/core/resources/icons/cloud9/light/vscode-help.svg deleted file mode 100644 index 81c89a3b963..00000000000 --- a/packages/core/resources/icons/cloud9/light/vscode-help.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts index ab053333432..4c29f005557 100644 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ b/packages/core/src/amazonq/commons/connector/baseMessenger.ts @@ -85,6 +85,7 @@ export class Messenger { type: 'answer', tabID: tabID, message: i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), + disableChatInput: true, }) this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled')) } diff --git a/packages/core/src/amazonq/commons/controllers/contentController.ts b/packages/core/src/amazonq/commons/controllers/contentController.ts index cdf19fe86b4..1380253f8eb 100644 --- a/packages/core/src/amazonq/commons/controllers/contentController.ts +++ b/packages/core/src/amazonq/commons/controllers/contentController.ts @@ -16,6 +16,7 @@ import { getSelectionFromRange, } from '../../../shared/utilities/textDocumentUtilities' import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, ToolkitError } from '../../../shared' +import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker' export class ContentProvider implements vscode.TextDocumentContentProvider { constructor(private uri: vscode.Uri) {} @@ -41,6 +42,7 @@ export class EditorContentController { ) { const editor = window.activeTextEditor if (editor) { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const cursorStart = editor.selection.active const indentRange = new vscode.Range(new vscode.Position(cursorStart.line, 0), cursorStart) // use the user editor intent if the position to the left of cursor is just space or tab @@ -66,9 +68,11 @@ export class EditorContentController { if (appliedEdits) { trackCodeEdit(editor, cursorStart) } + UserWrittenCodeTracker.instance.onQFinishesEdits() }, (e) => { getLogger().error('TextEditor.edit failed: %s', (e as Error).message) + UserWrittenCodeTracker.instance.onQFinishesEdits() } ) } @@ -97,6 +101,7 @@ export class EditorContentController { if (filePath && message?.code?.trim().length > 0 && selection) { try { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const doc = await vscode.workspace.openTextDocument(filePath) const code = getIndentedCode(message, doc, selection) @@ -130,6 +135,8 @@ export class EditorContentController { const wrappedError = ChatDiffError.chain(error, `Failed to Accept Diff`, { code: chatDiffCode }) getLogger().error('%s: Failed to open diff view %s', chatDiffCode, getErrorMsg(wrappedError, true)) throw wrappedError + } finally { + UserWrittenCodeTracker.instance.onQFinishesEdits() } } } diff --git a/packages/core/src/amazonq/commons/diff.ts b/packages/core/src/amazonq/commons/diff.ts index ed6642b9192..aac25d236d8 100644 --- a/packages/core/src/amazonq/commons/diff.ts +++ b/packages/core/src/amazonq/commons/diff.ts @@ -33,12 +33,18 @@ export function createAmazonQUri(path: string, tabId: string, scheme: string) { return vscode.Uri.from({ scheme: scheme, path, query: `tabID=${tabId}` }) } -export async function computeDiff(leftPath: string, rightPath: string, tabId: string, scheme: string) { +export async function computeDiff( + leftPath: string, + rightPath: string, + tabId: string, + scheme: string, + reportedChanges?: string +) { const { left, right } = await getFileDiffUris(leftPath, rightPath, tabId, scheme) const leftFile = await vscode.workspace.openTextDocument(left) const rightFile = await vscode.workspace.openTextDocument(right) - const changes = diffLines(leftFile.getText(), rightFile.getText(), { + const changes = diffLines(reportedChanges ?? leftFile.getText(), rightFile.getText(), { ignoreWhitespace: true, }) diff --git a/packages/core/src/amazonq/commons/types.ts b/packages/core/src/amazonq/commons/types.ts index f5724a13872..da6503b262f 100644 --- a/packages/core/src/amazonq/commons/types.ts +++ b/packages/core/src/amazonq/commons/types.ts @@ -24,6 +24,9 @@ export enum FollowUpTypes { NewTask = 'NewTask', CloseSession = 'CloseSession', SendFeedback = 'SendFeedback', + AcceptAutoBuild = 'AcceptAutoBuild', + DenyAutoBuild = 'DenyAutoBuild', + GenerateDevFile = 'GenerateDevFile', // Doc CreateDocumentation = 'CreateDocumentation', ChooseFolder = 'ChooseFolder', diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index cd4ec424365..9ca9af7687c 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -25,6 +25,7 @@ export { init as gumbyChatAppInit } from '../amazonqGumby/app' export { init as testChatAppInit } from '../amazonqTest/app' export { init as docChatAppInit } from '../amazonqDoc/app' export { amazonQHelpUrl } from '../shared/constants' +export * as webviewConstants from './webview/ui/texts/constants' export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/statusBarMenu' export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands' export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens' diff --git a/packages/core/src/amazonq/lsp/lspController.ts b/packages/core/src/amazonq/lsp/lspController.ts index d3f8960d1fc..b166b7c0b09 100644 --- a/packages/core/src/amazonq/lsp/lspController.ts +++ b/packages/core/src/amazonq/lsp/lspController.ts @@ -58,7 +58,7 @@ export interface Manifest { } 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 supportedLspServerVersions = ['0.1.35'] const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node' diff --git a/packages/core/src/amazonq/onboardingPage/walkthrough.ts b/packages/core/src/amazonq/onboardingPage/walkthrough.ts index 30e31ac1055..466b6970b91 100644 --- a/packages/core/src/amazonq/onboardingPage/walkthrough.ts +++ b/packages/core/src/amazonq/onboardingPage/walkthrough.ts @@ -50,11 +50,12 @@ export const walkthroughInlineSuggestionsExample = Commands.declare( `_aws.amazonq.walkthrough.inlineSuggestionsExample`, () => async () => { const fileName = 'AmazonQ_generate_suggestion.py' - const fileContents = `# TODO: place your cursor at the end of line 5 and press Enter to generate a suggestion. + const fileContents = `# TODO: place your cursor at the end of line 6 and press Enter to generate a suggestion. # Tip: press tab to accept the suggestion fake_users = [ - { "name": "User 1", "id": "user1", "city": "San Francisco", "state": "CA" },` + { "name": "User 1", "id": "user1", "city": "San Francisco", "state": "CA" }, +]` const uri = vscode.Uri.parse(`untitled:${fileName}`) const document = await vscode.workspace.openTextDocument(uri) diff --git a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts index 6acc250a25f..8acd04e8953 100644 --- a/packages/core/src/amazonq/webview/messages/messageDispatcher.ts +++ b/packages/core/src/amazonq/webview/messages/messageDispatcher.ts @@ -48,9 +48,9 @@ export function dispatchWebViewMessagesToApps( AmazonQChatMessageDuration.stopChatMessageTelemetry(msg) return } - case 'open-user-guide': { - const { userGuideLink } = msg - void openUrl(Uri.parse(userGuideLink)) + case 'open-link': { + const { link } = msg + void openUrl(Uri.parse(link)) return } case 'send-telemetry': { diff --git a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts index c0f031509d3..e9378512995 100644 --- a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts @@ -130,8 +130,8 @@ export class Connector { this.sendMessageToExtension(createClickTelemetry(`amazonq-explore-${actionId}`)) this.sendMessageToExtension({ - command: 'open-user-guide', - userGuideLink, + command: 'open-link', + link: userGuideLink, }) } diff --git a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts index cefc2b8818f..ae179fd6c41 100644 --- a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts @@ -13,6 +13,7 @@ import { TabsStorage, TabType } from '../storages/tabsStorage' import { TestMessageType } from '../../../../amazonqTest/chat/views/connector/connector' import { ChatPayload } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' +import { FollowUpTypes } from '../../../commons/types' export interface ConnectorProps extends BaseConnectorProps { sendMessageToExtension: (message: ExtensionMessage) => void @@ -107,15 +108,43 @@ export class Connector extends BaseConnector { } onFileDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - // TODO: add this back once we can advance flow from here - // this.sendMessageToExtension({ - // command: 'open-diff', - // tabID, - // filePath, - // deleted, - // messageId, - // tabType: 'testgen', - // }) + if (this.onChatAnswerReceived === undefined) { + return + } + // Open diff view + this.sendMessageToExtension({ + command: 'open-diff', + tabID, + filePath, + deleted, + messageId, + tabType: 'testgen', + }) + this.onChatAnswerReceived( + tabID, + { + type: ChatItemType.ANSWER, + messageId: messageId, + followUp: { + text: ' ', + options: [ + { + type: FollowUpTypes.AcceptCode, + pillText: 'Accept', + status: 'success', + icon: MynahIcons.OK, + }, + { + type: FollowUpTypes.RejectCode, + pillText: 'Reject', + status: 'error', + icon: MynahIcons.REVERT, + }, + ], + }, + }, + {} + ) } private processChatMessage = async (messageData: any): Promise => { diff --git a/packages/core/src/amazonq/webview/ui/commands.ts b/packages/core/src/amazonq/webview/ui/commands.ts index d668cb5d3b7..bdf2490b3a1 100644 --- a/packages/core/src/amazonq/webview/ui/commands.ts +++ b/packages/core/src/amazonq/webview/ui/commands.ts @@ -39,7 +39,7 @@ type MessageCommand = | 'store-code-result-message-id' | 'start-test-gen' | 'review' - | 'open-user-guide' + | 'open-link' | 'send-telemetry' | 'update-welcome-count' diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index a0ddb355d87..e7d2d47bf24 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -131,6 +131,13 @@ export class Connector { } } + onLinkClick = (link: string): void => { + this.sendMessageToExtension({ + command: 'open-link', + link, + }) + } + onResponseBodyLinkClick = (tabID: string, messageId: string, link: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { case 'cwc': diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index d056871bda2..74ada92cc67 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -106,6 +106,10 @@ export const createMynahUI = ( let isDocEnabled = amazonQEnabled + let featureConfigs: Map = tryNewMap(featureConfigsSerialized) + + const highlightCommand = featureConfigs.get('highlightCommand') + let tabDataGenerator = new TabDataGenerator({ isFeatureDevEnabled, isGumbyEnabled, @@ -113,6 +117,7 @@ export const createMynahUI = ( isTestEnabled, isDocEnabled, disabledCommands, + commandHighlight: highlightCommand, }) // eslint-disable-next-line prefer-const @@ -124,9 +129,6 @@ export const createMynahUI = ( // eslint-disable-next-line prefer-const let messageController: MessageController - // @ts-ignore - let featureConfigs: Map = tryNewMap(featureConfigsSerialized) - function getCodeBlockActions(messageData: any) { // Show ViewDiff and AcceptDiff for allowedCommands in CWC const isEnabled = featureConfigs.get('ViewDiffInChat')?.variation === 'TREATMENT' @@ -199,6 +201,7 @@ export const createMynahUI = ( isTestEnabled, isDocEnabled, disabledCommands, + commandHighlight: highlightCommand, }) featureConfigs = tryNewMap(featureConfigsSerialized) @@ -831,6 +834,12 @@ export const createMynahUI = ( mouseEvent?.stopImmediatePropagation() connector.onResponseBodyLinkClick(tabId, messageId, link) }, + onFormLinkClick: (link, mouseEvent) => { + mouseEvent?.preventDefault() + mouseEvent?.stopPropagation() + mouseEvent?.stopImmediatePropagation() + connector.onLinkClick(link) + }, onInfoLinkClick: (tabId: string, link: string, mouseEvent?: MouseEvent) => { mouseEvent?.preventDefault() mouseEvent?.stopPropagation() diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index b3263218c1d..a6d31e715df 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemType, MynahUIDataModel, QuickActionCommandGroup } from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { FollowUpGenerator } from '../followUps/generator' import { QuickActionGenerator } from '../quickActions/generator' import { TabTypeDataMap } from './constants' import { agentWalkthroughDataModel } from '../walkthrough/agent' +import { FeatureContext } from '../../../../shared' export interface TabDataGeneratorProps { isFeatureDevEnabled: boolean @@ -17,11 +18,13 @@ export interface TabDataGeneratorProps { isTestEnabled: boolean isDocEnabled: boolean disabledCommands?: string[] + commandHighlight?: FeatureContext } export class TabDataGenerator { private followUpsGenerator: FollowUpGenerator public quickActionsGenerator: QuickActionGenerator + private highlightCommand?: FeatureContext constructor(props: TabDataGeneratorProps) { this.followUpsGenerator = new FollowUpGenerator() @@ -33,6 +36,7 @@ export class TabDataGenerator { isDocEnabled: props.isDocEnabled, disableCommands: props.disabledCommands, }) + this.highlightCommand = props.commandHighlight } public getTabData(tabType: TabType, needWelcomeMessages: boolean, taskName?: string): MynahUIDataModel { @@ -50,7 +54,7 @@ export class TabDataGenerator { 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/).', quickActionCommands: this.quickActionsGenerator.generateForTab(tabType), promptInputPlaceholder: TabTypeDataMap[tabType].placeholder, - contextCommands: TabTypeDataMap[tabType].contextCommands, + contextCommands: this.getContextCommands(tabType), chatItems: needWelcomeMessages ? [ { @@ -66,4 +70,32 @@ export class TabDataGenerator { } return tabData } + + private getContextCommands(tabType: TabType): QuickActionCommandGroup[] | undefined { + if (tabType === 'agentWalkthrough' || tabType === 'welcome') { + return + } + + const commandName = this.highlightCommand?.value.stringValue + if (commandName === undefined || commandName === '') { + return TabTypeDataMap[tabType].contextCommands + } else { + const commandHighlight: QuickActionCommandGroup = { + groupName: 'Additional Commands', + commands: [ + { + command: commandName, + description: this.highlightCommand?.variation, + }, + ], + } + + const contextCommands = TabTypeDataMap[tabType].contextCommands + if (contextCommands === undefined) { + return [commandHighlight] + } else { + return [...contextCommands, commandHighlight] + } + } + } } diff --git a/packages/core/src/amazonq/webview/ui/texts/constants.ts b/packages/core/src/amazonq/webview/ui/texts/constants.ts index d907308b8c3..23d733bfd4f 100644 --- a/packages/core/src/amazonq/webview/ui/texts/constants.ts +++ b/packages/core/src/amazonq/webview/ui/texts/constants.ts @@ -8,6 +8,8 @@ export const uiComponentsTexts = { copy: 'Copy', insertAtCursorLabel: 'Insert at cursor', feedbackFormTitle: 'Report an issue', + feedbackFormDescription: + '_Feedback is anonymous. For issue updates, please contact us on [GitHub](https://github.com/aws/aws-toolkit-vscode/issues/new/choose)._', feedbackFormOptionsLabel: 'What type of issue would you like to report?', feedbackFormCommentLabel: 'Description of issue (optional):', feedbackThanks: 'Thanks for your feedback!', diff --git a/packages/core/src/amazonqDoc/constants.ts b/packages/core/src/amazonqDoc/constants.ts index 5d1d938c940..90284a90648 100644 --- a/packages/core/src/amazonqDoc/constants.ts +++ b/packages/core/src/amazonqDoc/constants.ts @@ -92,6 +92,43 @@ export const FolderSelectorFollowUps = [ }, ] +export const CodeChangeFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.accept'), + prompt: i18n('AWS.amazonq.doc.pillText.accept'), + type: FollowUpTypes.AcceptChanges, + icon: 'ok' as MynahIcons, + status: 'success' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.makeChanges'), + prompt: i18n('AWS.amazonq.doc.pillText.makeChanges'), + type: FollowUpTypes.MakeChanges, + icon: 'refresh' as MynahIcons, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.reject'), + prompt: i18n('AWS.amazonq.doc.pillText.reject'), + type: FollowUpTypes.RejectChanges, + icon: 'cancel' as MynahIcons, + status: 'error' as Status, + }, +] + +export const NewSessionFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.newTask'), + type: FollowUpTypes.NewTask, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), + type: FollowUpTypes.CloseSession, + status: 'info' as Status, + }, +] + export const SynchronizeDocumentation = { pillText: i18n('AWS.amazonq.doc.pillText.update'), prompt: i18n('AWS.amazonq.doc.pillText.update'), diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts index 8246a011fbe..2f31346d9cd 100644 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ b/packages/core/src/amazonqDoc/controllers/chat/controller.ts @@ -10,7 +10,9 @@ import { EditDocumentation, FolderSelectorFollowUps, Mode, + NewSessionFollowUps, SynchronizeDocumentation, + CodeChangeFollowUps, docScheme, featureName, findReadmePath, @@ -22,7 +24,6 @@ import { Session } from '../../session/session' import { i18n } from '../../../shared/i18n-helper' import path from 'path' import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' -import { MynahIcons } from '@aws/mynah-ui' import { MonthlyConversationLimitError, @@ -43,6 +44,7 @@ import { import { getPathsFromZipFilePath } from '../../../amazonqFeatureDev/util/files' import { FollowUpTypes } from '../../../amazonq/commons/types' import { DocGenerationTask } from '../docGenerationTask' +import { DevPhase } from '../../types' export interface ChatControllerEventEmitters { readonly processHumanChatMessage: EventEmitter @@ -226,8 +228,6 @@ export class DocController { return } - this.docGenerationTask.userIdentity = AuthUtil.instance.conn?.id - const sendFolderConfirmationMessage = (message: string) => { this.messenger.sendFolderConfirmationMessage( data.tabID, @@ -287,29 +287,18 @@ export class DocController { break case FollowUpTypes.AcceptChanges: this.docGenerationTask.userDecision = 'ACCEPT' - await this.sendDocGenerationEvent(data) + await this.sendDocAcceptanceEvent(data) await this.insertCode(data) return case FollowUpTypes.RejectChanges: this.docGenerationTask.userDecision = 'REJECT' - await this.sendDocGenerationEvent(data) + await this.sendDocAcceptanceEvent(data) this.messenger.sendAnswer({ type: 'answer', tabID: data?.tabID, disableChatInput: true, message: 'Your changes have been discarded.', - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) break case FollowUpTypes.ProceedFolderSelection: @@ -333,7 +322,7 @@ export class DocController { } break case FollowUpTypes.CancelFolderSelection: - this.docGenerationTask.reset() + this.docGenerationTask.folderLevel = 'ENTIRE_WORKSPACE' return this.tabOpened(data) } }) @@ -412,13 +401,19 @@ export class DocController { const errorMessage = createUserFacingErrorMessage(`${err.cause?.message ?? err.message}`) // eslint-disable-next-line unicorn/no-null this.messenger.sendUpdatePromptProgress(message.tabID, null) + if (err.constructor.name === MonthlyConversationLimitError.name) { + this.messenger.sendMonthlyLimitError(message.tabID) + } else { + const enableUserInput = this.mode === Mode.EDIT && err.remainingIterations > 0 - switch (err.constructor.name) { - case MonthlyConversationLimitError.name: - this.messenger.sendMonthlyLimitError(message.tabID) - break - default: - this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, false) + this.messenger.sendErrorMessage( + errorMessage, + message.tabID, + 0, + session?.conversationIdUnsafe, + false, + enableUserInput + ) } } @@ -427,8 +422,6 @@ export class DocController { await this.onDocsGeneration(session, message.message, message.tabID) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -461,12 +454,8 @@ export class DocController { } await this.generateDocumentation({ message, session }) - this.messenger.sendChatInputEnabled(message?.tabID, false) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -498,7 +487,7 @@ export class DocController { session.isAuthenticating = true return } - this.docGenerationTask.numberOfNavigation += 1 + this.docGenerationTask.numberOfNavigations += 1 this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, @@ -590,40 +579,32 @@ export class DocController { this.messenger.sendAnswer({ type: 'answer', tabID: tabID, - message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${i18n('AWS.amazonq.doc.answer.codeResult')}`, + message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${remainingIterations > 0 ? i18n('AWS.amazonq.doc.answer.codeResult') : i18n('AWS.amazonq.doc.answer.acceptOrReject')}`, disableChatInput: true, }) - } - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - disableChatInput: true, - followUps: [ - { - pillText: 'Accept', - prompt: 'Accept', - type: FollowUpTypes.AcceptChanges, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: 'Make changes', - prompt: 'Make changes', - type: FollowUpTypes.MakeChanges, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - { - pillText: 'Reject', - prompt: 'Reject', - type: FollowUpTypes.RejectChanges, - icon: 'cancel' as MynahIcons, - status: 'error', - }, - ], - tabID: tabID, - }) + this.messenger.sendAnswer({ + message: undefined, + type: 'system-prompt', + disableChatInput: true, + followUps: + remainingIterations > 0 + ? CodeChangeFollowUps + : CodeChangeFollowUps.filter((followUp) => followUp.type !== FollowUpTypes.MakeChanges), + tabID: tabID, + }) + } + if (session?.state.phase === DevPhase.CODEGEN) { + const { totalGeneratedChars, totalGeneratedLines, totalGeneratedFiles } = + await session.countGeneratedContent(this.docGenerationTask.interactionType) + this.docGenerationTask.conversationId = session.conversationId + this.docGenerationTask.numberOfGeneratedChars = totalGeneratedChars + this.docGenerationTask.numberOfGeneratedLines = totalGeneratedLines + this.docGenerationTask.numberOfGeneratedFiles = totalGeneratedFiles + const docGenerationEvent = this.docGenerationTask.docGenerationEventBase() + + await session.sendDocTelemetryEvent(docGenerationEvent, 'generation') + } } finally { if (session?.state?.tokenSource?.token.isCancellationRequested) { await this.newTask({ tabID }) @@ -642,10 +623,8 @@ export class DocController { type: 'answer', tabID: message.tabID, message: 'Follow instructions to re-authenticate ...', + disableChatInput: true, }) - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) } private tabClosed(message: any) { @@ -670,18 +649,7 @@ export class DocController { type: 'answer', disableChatInput: true, tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) @@ -694,18 +662,18 @@ export class DocController { ) } } - private async sendDocGenerationEvent(message: any) { + private async sendDocAcceptanceEvent(message: any) { const session = await this.sessionStorage.getSession(message.tabID) this.docGenerationTask.conversationId = session.conversationId const { totalAddedChars, totalAddedLines, totalAddedFiles } = await session.countAddedContent( this.docGenerationTask.interactionType ) - this.docGenerationTask.numberOfAddChars = totalAddedChars - this.docGenerationTask.numberOfAddLines = totalAddedLines - this.docGenerationTask.numberOfAddFiles = totalAddedFiles - const docGenerationEvent = this.docGenerationTask.docGenerationEventBase() + this.docGenerationTask.numberOfAddedChars = totalAddedChars + this.docGenerationTask.numberOfAddedLines = totalAddedLines + this.docGenerationTask.numberOfAddedFiles = totalAddedFiles + const docAcceptanceEvent = this.docGenerationTask.docAcceptanceEventBase() - await session.sendDocGenerationTelemetryEvent(docGenerationEvent) + await session.sendDocTelemetryEvent(docAcceptanceEvent, 'acceptance') } private processLink(message: any) { void openUrl(vscode.Uri.parse(message.link)) diff --git a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts b/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts index 848de727570..be345884fd2 100644 --- a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts +++ b/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts @@ -3,24 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ import { - DocGenerationEvent, - DocGenerationFolderLevel, - DocGenerationInteractionType, - DocGenerationUserDecision, + DocFolderLevel, + DocInteractionType, + DocUserDecision, + DocV2AcceptanceEvent, + DocV2GenerationEvent, } from '../../codewhisperer/client/codewhispereruserclient' import { getLogger } from '../../shared' export class DocGenerationTask { // Telemetry fields public conversationId?: string - public numberOfAddChars?: number - public numberOfAddLines?: number - public numberOfAddFiles?: number - public userDecision?: DocGenerationUserDecision - public interactionType?: DocGenerationInteractionType - public userIdentity?: string - public numberOfNavigation = 0 - public folderLevel: DocGenerationFolderLevel = 'ENTIRE_WORKSPACE' + public numberOfAddedChars?: number + public numberOfAddedLines?: number + public numberOfAddedFiles?: number + public numberOfGeneratedChars?: number + public numberOfGeneratedLines?: number + public numberOfGeneratedFiles?: number + public userDecision?: DocUserDecision + public interactionType?: DocInteractionType + public numberOfNavigations = 0 + public folderLevel: DocFolderLevel = 'ENTIRE_WORKSPACE' constructor(conversationId?: string) { this.conversationId = conversationId @@ -32,17 +35,36 @@ export class DocGenerationTask { .map(([key]) => key) if (undefinedProps.length > 0) { - getLogger().debug(`DocGenerationEvent has undefined properties: ${undefinedProps.join(', ')}`) + getLogger().debug(`DocV2GenerationEvent has undefined properties: ${undefinedProps.join(', ')}`) } - const event: DocGenerationEvent = { + const event: DocV2GenerationEvent = { conversationId: this.conversationId ?? '', - numberOfAddChars: this.numberOfAddChars, - numberOfAddLines: this.numberOfAddLines, - numberOfAddFiles: this.numberOfAddFiles, - userDecision: this.userDecision, + numberOfGeneratedChars: this.numberOfGeneratedChars ?? 0, + numberOfGeneratedLines: this.numberOfGeneratedLines ?? 0, + numberOfGeneratedFiles: this.numberOfGeneratedFiles ?? 0, interactionType: this.interactionType, - userIdentity: this.userIdentity, - numberOfNavigation: this.numberOfNavigation, + numberOfNavigations: this.numberOfNavigations, + folderLevel: this.folderLevel, + } + return event + } + + public docAcceptanceEventBase() { + const undefinedProps = Object.entries(this) + .filter(([key, value]) => value === undefined) + .map(([key]) => key) + + if (undefinedProps.length > 0) { + getLogger().debug(`DocV2AcceptanceEvent has undefined properties: ${undefinedProps.join(', ')}`) + } + const event: DocV2AcceptanceEvent = { + conversationId: this.conversationId ?? '', + numberOfAddedChars: this.numberOfAddedChars ?? 0, + numberOfAddedLines: this.numberOfAddedLines ?? 0, + numberOfAddedFiles: this.numberOfAddedFiles ?? 0, + userDecision: this.userDecision ?? 'ACCEPTED', + interactionType: this.interactionType ?? 'GENERATE_README', + numberOfNavigations: this.numberOfNavigations ?? 0, folderLevel: this.folderLevel, } return event @@ -50,13 +72,15 @@ export class DocGenerationTask { public reset() { this.conversationId = undefined - this.numberOfAddChars = undefined - this.numberOfAddLines = undefined - this.numberOfAddFiles = undefined + this.numberOfAddedChars = undefined + this.numberOfAddedLines = undefined + this.numberOfAddedFiles = undefined + this.numberOfGeneratedChars = undefined + this.numberOfGeneratedLines = undefined + this.numberOfGeneratedFiles = undefined this.userDecision = undefined this.interactionType = undefined - this.userIdentity = undefined - this.numberOfNavigation = 0 + this.numberOfNavigations = 0 this.folderLevel = 'ENTIRE_WORKSPACE' } } diff --git a/packages/core/src/amazonqDoc/errors.ts b/packages/core/src/amazonqDoc/errors.ts index d9794f16327..11a6514a616 100644 --- a/packages/core/src/amazonqDoc/errors.ts +++ b/packages/core/src/amazonqDoc/errors.ts @@ -7,69 +7,57 @@ import { ToolkitError } from '../shared/errors' import { i18n } from '../shared/i18n-helper' export class DocServiceError extends ToolkitError { - constructor(message: string, code: string) { + remainingIterations?: number + constructor(message: string, code: string, remainingIterations?: number) { super(message, { code }) + this.remainingIterations = remainingIterations } } -export class ReadmeTooLargeError extends ToolkitError { +export class ReadmeTooLargeError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), { - code: ReadmeTooLargeError.name, - }) + super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), ReadmeTooLargeError.name) } } -export class ReadmeUpdateTooLargeError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), { - code: ReadmeUpdateTooLargeError.name, - }) +export class ReadmeUpdateTooLargeError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), ReadmeUpdateTooLargeError.name, remainingIterations) } } -export class WorkspaceEmptyError extends ToolkitError { +export class WorkspaceEmptyError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), { - code: WorkspaceEmptyError.name, - }) + super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), WorkspaceEmptyError.name) } } -export class NoChangeRequiredException extends ToolkitError { +export class NoChangeRequiredException extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), { - code: NoChangeRequiredException.name, - }) + super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), NoChangeRequiredException.name) } } -export class PromptRefusalException extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptRefusal'), { - code: PromptRefusalException.name, - }) +export class PromptRefusalException extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptRefusal'), PromptRefusalException.name, remainingIterations) } } -export class ContentLengthError extends ToolkitError { +export class ContentLengthError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.contentLengthError'), { code: ContentLengthError.name }) + super(i18n('AWS.amazonq.doc.error.contentLengthError'), ContentLengthError.name) } } -export class PromptTooVagueError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptTooVague'), { - code: PromptTooVagueError.name, - }) +export class PromptTooVagueError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptTooVague'), PromptTooVagueError.name, remainingIterations) } } -export class PromptUnrelatedError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptUnrelated'), { - code: PromptUnrelatedError.name, - }) +export class PromptUnrelatedError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptUnrelated'), PromptUnrelatedError.name, remainingIterations) } } diff --git a/packages/core/src/amazonqDoc/messenger.ts b/packages/core/src/amazonqDoc/messenger.ts index 09be3dd11fb..f28e5e9060b 100644 --- a/packages/core/src/amazonqDoc/messenger.ts +++ b/packages/core/src/amazonqDoc/messenger.ts @@ -4,10 +4,9 @@ */ import { Messenger } from '../amazonq/commons/connector/baseMessenger' import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { FollowUpTypes } from '../amazonq/commons/types' import { messageWithConversationId } from '../amazonqFeatureDev' import { i18n } from '../shared/i18n-helper' -import { docGenerationProgressMessage, DocGenerationStep, Mode } from './constants' +import { docGenerationProgressMessage, DocGenerationStep, Mode, NewSessionFollowUps } from './constants' import { inProgress } from './types' export class DocMessenger extends Messenger { @@ -48,25 +47,19 @@ export class DocMessenger extends Messenger { tabID: string, _retries: number, conversationId?: string, - _showDefaultMessage?: boolean + _showDefaultMessage?: boolean, + enableUserInput?: boolean ) { + if (enableUserInput) { + this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) + this.sendChatInputEnabled(tabID, true) + } this.sendAnswer({ type: 'answer', tabID: tabID, message: errorMessage + messageWithConversationId(conversationId), - }) - - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID, + followUps: enableUserInput ? [] : NewSessionFollowUps, + disableChatInput: !enableUserInput, }) } } diff --git a/packages/core/src/amazonqDoc/session/session.ts b/packages/core/src/amazonqDoc/session/session.ts index 309a6fd7408..4396e6e5e6b 100644 --- a/packages/core/src/amazonqDoc/session/session.ts +++ b/packages/core/src/amazonqDoc/session/session.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { featureName, Mode } from '../constants' +import { docScheme, featureName, Mode } from '../constants' import { DeletedFileInfo, Interaction, NewFileInfo, SessionState, SessionStateConfig } from '../types' import { PrepareCodeGenState } from './sessionState' import { telemetry } from '../../shared/telemetry/telemetry' @@ -19,13 +19,14 @@ import { logWithConversationId } from '../../amazonqFeatureDev/userFacingText' import { ConversationIdNotFoundError } from '../../amazonqFeatureDev/errors' import { referenceLogText } from '../../amazonqFeatureDev/constants' import { - DocGenerationEvent, - DocGenerationInteractionType, + DocInteractionType, + DocV2AcceptanceEvent, + DocV2GenerationEvent, SendTelemetryEventRequest, } from '../../codewhisperer/client/codewhispereruserclient' -import { getDiffCharsAndLines } from '../../shared/utilities/diffUtils' import { getClientId, getOperatingSystem, getOptOutPreference } from '../../shared/telemetry/util' import { DocMessenger } from '../messenger' +import { computeDiff } from '../../amazonq/commons/diff' export class Session { private _state?: SessionState | Omit @@ -38,6 +39,7 @@ export class Session { // Used to keep track of whether or not the current session is currently authenticating/needs authenticating public isAuthenticating: boolean + private _reportedDocChanges: { [key: string]: string } = {} constructor( public readonly config: SessionConfig, @@ -177,41 +179,92 @@ export class Session { } } - public async countAddedContent(interactionType?: DocGenerationInteractionType) { - let totalAddedChars = 0 - let totalAddedLines = 0 - let totalAddedFiles = 0 + private getFromReportedChanges(filepath: NewFileInfo) { + const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` + return this._reportedDocChanges[key] + } - for (const filePath of this.state.filePaths?.filter((i) => !i.rejected) ?? []) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - const uri = filePath.virtualMemoryUri - const content = await this.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - totalAddedFiles += 1 + private addToReportedChanges(filepath: NewFileInfo) { + const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` + this._reportedDocChanges[key] = filepath.fileContent + } - if ((await fs.exists(absolutePath)) && interactionType === 'UPDATE_README') { - const existingContent = await fs.readFileText(absolutePath) - const { addedChars, addedLines } = getDiffCharsAndLines(existingContent, decodedContent) - totalAddedChars += addedChars - totalAddedLines += addedLines + public async countGeneratedContent(interactionType?: DocInteractionType) { + let totalGeneratedChars = 0 + let totalGeneratedLines = 0 + let totalGeneratedFiles = 0 + const filePaths = this.state.filePaths ?? [] + + for (const filePath of filePaths) { + const reportedDocChange = this.getFromReportedChanges(filePath) + if (interactionType === 'GENERATE_README') { + if (reportedDocChange) { + const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) + totalGeneratedChars += charsAdded + totalGeneratedLines += linesAdded + } else { + // If no changes are reported, this is the initial README generation and no comparison with existing files is needed + const fileContent = filePath.fileContent + totalGeneratedChars += fileContent.length + totalGeneratedLines += fileContent.split('\n').length + } } else { - totalAddedChars += decodedContent.length - totalAddedLines += decodedContent.split('\n').length + const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) + totalGeneratedChars += charsAdded + totalGeneratedLines += linesAdded } + this.addToReportedChanges(filePath) + totalGeneratedFiles += 1 + } + return { + totalGeneratedChars, + totalGeneratedLines, + totalGeneratedFiles, } + } + public async countAddedContent(interactionType?: DocInteractionType) { + let totalAddedChars = 0 + let totalAddedLines = 0 + let totalAddedFiles = 0 + const newFilePaths = + this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? [] + + for (const filePath of newFilePaths) { + if (interactionType === 'GENERATE_README') { + const fileContent = filePath.fileContent + totalAddedChars += fileContent.length + totalAddedLines += fileContent.split('\n').length + } else { + const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) + totalAddedChars += charsAdded + totalAddedLines += linesAdded + } + totalAddedFiles += 1 + } return { totalAddedChars, totalAddedLines, totalAddedFiles, } } - public async sendDocGenerationTelemetryEvent(docGenerationEvent: DocGenerationEvent) { + + public async computeFilePathDiff(filePath: NewFileInfo, reportedChanges?: string) { + const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}` + const rightPath = filePath.virtualMemoryUri.path + const diff = await computeDiff(leftPath, rightPath, this.tabID, docScheme, reportedChanges) + return { leftPath, rightPath, ...diff } + } + + public async sendDocTelemetryEvent( + telemetryEvent: DocV2GenerationEvent | DocV2AcceptanceEvent, + eventType: 'generation' | 'acceptance' + ) { const client = await this.proxyClient.getClient() try { const params: SendTelemetryEventRequest = { telemetryEvent: { - docGenerationEvent, + [eventType === 'generation' ? 'docV2GenerationEvent' : 'docV2AcceptanceEvent']: telemetryEvent, }, optOutPreference: getOptOutPreference(), userContext: { @@ -222,13 +275,14 @@ export class Session { ideVersion: extensionVersion, }, } + const response = await client.sendTelemetryEvent(params).promise() getLogger().debug( - `${featureName}: successfully sent docGenerationEvent: ConversationId: ${docGenerationEvent.conversationId} RequestId: ${response.$response.requestId}` + `${featureName}: successfully sent docV2${eventType === 'generation' ? 'GenerationEvent' : 'AcceptanceEvent'}: ConversationId: ${telemetryEvent.conversationId} RequestId: ${response.$response.requestId}` ) } catch (e) { getLogger().error( - `${featureName}: failed to send doc generation telemetry: ${(e as Error).name}: ${ + `${featureName}: failed to send doc ${eventType} telemetry: ${(e as Error).name}: ${ (e as Error).message } RequestId: ${(e as any).requestId}` ) diff --git a/packages/core/src/amazonqDoc/session/sessionState.ts b/packages/core/src/amazonqDoc/session/sessionState.ts index b3404c7998a..7bf9c02e51b 100644 --- a/packages/core/src/amazonqDoc/session/sessionState.ts +++ b/packages/core/src/amazonqDoc/session/sessionState.ts @@ -97,7 +97,7 @@ abstract class CodeGenBase { ++pollingIteration ) { const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount + const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount || 0 const codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount getLogger().debug(`Codegen response: %O`, codegenResult) @@ -151,7 +151,7 @@ abstract class CodeGenBase { throw new ReadmeTooLargeError() } case codegenResult.codeGenerationStatusDetail?.includes('README_UPDATE_TOO_LARGE'): { - throw new ReadmeUpdateTooLargeError() + throw new ReadmeUpdateTooLargeError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_TOO_LARGE'): { throw new ContentLengthError() @@ -160,18 +160,19 @@ abstract class CodeGenBase { throw new WorkspaceEmptyError() } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_UNRELATED'): { - throw new PromptUnrelatedError() + throw new PromptUnrelatedError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_TOO_VAGUE'): { - throw new PromptTooVagueError() + throw new PromptTooVagueError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_REFUSAL'): { - throw new PromptRefusalException() + throw new PromptRefusalException(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { throw new DocServiceError( i18n('AWS.amazonq.doc.error.docGen.default'), - 'GuardrailsException' + 'GuardrailsException', + codeGenerationRemainingIterationCount ) } case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { @@ -186,7 +187,8 @@ abstract class CodeGenBase { case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { throw new DocServiceError( i18n('AWS.amazonq.featureDev.error.throttling'), - 'ThrottlingException' + 'ThrottlingException', + codeGenerationRemainingIterationCount ) } default: { diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json index 6f628b220cb..ff9670742ec 100644 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json @@ -856,18 +856,36 @@ "type": "structure", "required": ["conversationId", "messageId"], "members": { - "conversationId": { "shape": "ConversationId" }, - "messageId": { "shape": "MessageId" }, - "customizationArn": { "shape": "CustomizationArn" }, - "interactionType": { "shape": "ChatMessageInteractionType" }, + "conversationId": { + "shape": "ConversationId" + }, + "messageId": { + "shape": "MessageId" + }, + "customizationArn": { + "shape": "CustomizationArn" + }, + "interactionType": { + "shape": "ChatMessageInteractionType" + }, "interactionTarget": { "shape": "ChatInteractWithMessageEventInteractionTargetString" }, - "acceptedCharacterCount": { "shape": "Integer" }, - "acceptedLineCount": { "shape": "Integer" }, - "acceptedSnippetHasReference": { "shape": "Boolean" }, - "hasProjectLevelContext": { "shape": "Boolean" }, - "userIntent": { "shape": "UserIntent" } + "acceptedCharacterCount": { + "shape": "Integer" + }, + "acceptedLineCount": { + "shape": "Integer" + }, + "acceptedSnippetHasReference": { + "shape": "Boolean" + }, + "hasProjectLevelContext": { + "shape": "Boolean" + }, + "userIntent": { + "shape": "UserIntent" + } } }, "ChatInteractWithMessageEventInteractionTargetString": { @@ -954,16 +972,46 @@ "type": "structure", "required": ["programmingLanguage", "acceptedCharacterCount", "totalCharacterCount", "timestamp"], "members": { - "customizationArn": { "shape": "CustomizationArn" }, - "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "acceptedCharacterCount": { "shape": "PrimitiveInteger" }, - "totalCharacterCount": { "shape": "PrimitiveInteger" }, - "timestamp": { "shape": "Timestamp" }, - "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }, - "totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" }, - "totalNewCodeLineCount": { "shape": "PrimitiveInteger" } + "customizationArn": { + "shape": "CustomizationArn" + }, + "programmingLanguage": { + "shape": "ProgrammingLanguage" + }, + "acceptedCharacterCount": { + "shape": "PrimitiveInteger" + }, + "totalCharacterCount": { + "shape": "PrimitiveInteger" + }, + "timestamp": { + "shape": "Timestamp" + }, + "unmodifiedAcceptedCharacterCount": { + "shape": "PrimitiveInteger" + }, + "totalNewCodeCharacterCount": { + "shape": "PrimitiveInteger" + }, + "totalNewCodeLineCount": { + "shape": "PrimitiveInteger" + }, + "userWrittenCodeCharacterCount": { + "shape": "CodeCoverageEventUserWrittenCodeCharacterCountInteger" + }, + "userWrittenCodeLineCount": { + "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" + } } }, + "CodeCoverageEventUserWrittenCodeCharacterCountInteger": { + "type": "integer", + "min": 0 + }, + "CodeCoverageEventUserWrittenCodeLineCountInteger": { + "type": "integer", + "min": 0 + }, "CodeFixAcceptanceEvent": { "type": "structure", "required": ["jobId"], @@ -1088,10 +1136,18 @@ "type": "structure", "required": ["programmingLanguage", "codeScanJobId", "timestamp"], "members": { - "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "codeScanJobId": { "shape": "CodeScanJobId" }, - "timestamp": { "shape": "Timestamp" }, - "codeAnalysisScope": { "shape": "CodeAnalysisScope" } + "programmingLanguage": { + "shape": "ProgrammingLanguage" + }, + "codeScanJobId": { + "shape": "CodeScanJobId" + }, + "timestamp": { + "shape": "Timestamp" + }, + "codeAnalysisScope": { + "shape": "CodeAnalysisScope" + } } }, "CodeScanJobId": { @@ -1107,18 +1163,36 @@ "CodeScanRemediationsEvent": { "type": "structure", "members": { - "programmingLanguage": { "shape": "ProgrammingLanguage" }, + "programmingLanguage": { + "shape": "ProgrammingLanguage" + }, "CodeScanRemediationsEventType": { "shape": "CodeScanRemediationsEventType" }, - "timestamp": { "shape": "Timestamp" }, - "detectorId": { "shape": "String" }, - "findingId": { "shape": "String" }, - "ruleId": { "shape": "String" }, - "component": { "shape": "String" }, - "reason": { "shape": "String" }, - "result": { "shape": "String" }, - "includesFix": { "shape": "Boolean" } + "timestamp": { + "shape": "Timestamp" + }, + "detectorId": { + "shape": "String" + }, + "findingId": { + "shape": "String" + }, + "ruleId": { + "shape": "String" + }, + "component": { + "shape": "String" + }, + "reason": { + "shape": "String" + }, + "result": { + "shape": "String" + }, + "includesFix": { + "shape": "Boolean" + } } }, "CodeScanRemediationsEventType": { @@ -1129,11 +1203,21 @@ "type": "structure", "required": ["programmingLanguage", "codeScanJobId", "timestamp", "numberOfFindings"], "members": { - "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "codeScanJobId": { "shape": "CodeScanJobId" }, - "timestamp": { "shape": "Timestamp" }, - "numberOfFindings": { "shape": "PrimitiveInteger" }, - "codeAnalysisScope": { "shape": "CodeAnalysisScope" } + "programmingLanguage": { + "shape": "ProgrammingLanguage" + }, + "codeScanJobId": { + "shape": "CodeScanJobId" + }, + "timestamp": { + "shape": "Timestamp" + }, + "numberOfFindings": { + "shape": "PrimitiveInteger" + }, + "codeAnalysisScope": { + "shape": "CodeAnalysisScope" + } } }, "Completion": { @@ -1259,16 +1343,30 @@ "CreateUploadUrlRequest": { "type": "structure", "members": { - "contentMd5": { "shape": "CreateUploadUrlRequestContentMd5String" }, + "contentMd5": { + "shape": "CreateUploadUrlRequestContentMd5String" + }, "contentChecksum": { "shape": "CreateUploadUrlRequestContentChecksumString" }, - "contentChecksumType": { "shape": "ContentChecksumType" }, - "contentLength": { "shape": "CreateUploadUrlRequestContentLengthLong" }, - "artifactType": { "shape": "ArtifactType" }, - "uploadIntent": { "shape": "UploadIntent" }, - "uploadContext": { "shape": "UploadContext" }, - "uploadId": { "shape": "UploadId" } + "contentChecksumType": { + "shape": "ContentChecksumType" + }, + "contentLength": { + "shape": "CreateUploadUrlRequestContentLengthLong" + }, + "artifactType": { + "shape": "ArtifactType" + }, + "uploadIntent": { + "shape": "UploadIntent" + }, + "uploadContext": { + "shape": "UploadContext" + }, + "uploadId": { + "shape": "UploadId" + } } }, "CreateUploadUrlRequestContentChecksumString": { @@ -1422,33 +1520,154 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "DocFolderLevel": { + "type": "string", + "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] + }, "DocGenerationEvent": { "type": "structure", "required": ["conversationId"], "members": { - "conversationId": { "shape": "ConversationId" }, - "numberOfAddChars": { "shape": "PrimitiveInteger" }, - "numberOfAddLines": { "shape": "PrimitiveInteger" }, - "numberOfAddFiles": { "shape": "PrimitiveInteger" }, - "userDecision": { "shape": "DocGenerationUserDecision" }, - "interactionType": { "shape": "DocGenerationInteractionType" }, - "userIdentity": { "shape": "String" }, - "numberOfNavigation": { "shape": "PrimitiveInteger" }, - "folderLevel": { "shape": "DocGenerationFolderLevel" } + "conversationId": { + "shape": "ConversationId" + }, + "numberOfAddChars": { + "shape": "PrimitiveInteger" + }, + "numberOfAddLines": { + "shape": "PrimitiveInteger" + }, + "numberOfAddFiles": { + "shape": "PrimitiveInteger" + }, + "userDecision": { + "shape": "DocUserDecision" + }, + "interactionType": { + "shape": "DocInteractionType" + }, + "userIdentity": { + "shape": "String" + }, + "numberOfNavigation": { + "shape": "PrimitiveInteger" + }, + "folderLevel": { + "shape": "DocFolderLevel" + } } }, - "DocGenerationFolderLevel": { - "type": "string", - "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] - }, - "DocGenerationInteractionType": { + "DocInteractionType": { "type": "string", "enum": ["GENERATE_README", "UPDATE_README", "EDIT_README"] }, - "DocGenerationUserDecision": { + "DocUserDecision": { "type": "string", "enum": ["ACCEPT", "REJECT"] }, + "DocV2AcceptanceEvent": { + "type": "structure", + "required": [ + "conversationId", + "numberOfAddedChars", + "numberOfAddedLines", + "numberOfAddedFiles", + "userDecision", + "interactionType", + "numberOfNavigations", + "folderLevel" + ], + "members": { + "conversationId": { + "shape": "ConversationId" + }, + "numberOfAddedChars": { + "shape": "DocV2AcceptanceEventNumberOfAddedCharsInteger" + }, + "numberOfAddedLines": { + "shape": "DocV2AcceptanceEventNumberOfAddedLinesInteger" + }, + "numberOfAddedFiles": { + "shape": "DocV2AcceptanceEventNumberOfAddedFilesInteger" + }, + "userDecision": { + "shape": "DocUserDecision" + }, + "interactionType": { + "shape": "DocInteractionType" + }, + "numberOfNavigations": { + "shape": "DocV2AcceptanceEventNumberOfNavigationsInteger" + }, + "folderLevel": { + "shape": "DocFolderLevel" + } + } + }, + "DocV2AcceptanceEventNumberOfAddedCharsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfAddedFilesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfAddedLinesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfNavigationsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEvent": { + "type": "structure", + "required": [ + "conversationId", + "numberOfGeneratedChars", + "numberOfGeneratedLines", + "numberOfGeneratedFiles" + ], + "members": { + "conversationId": { + "shape": "ConversationId" + }, + "numberOfGeneratedChars": { + "shape": "DocV2GenerationEventNumberOfGeneratedCharsInteger" + }, + "numberOfGeneratedLines": { + "shape": "DocV2GenerationEventNumberOfGeneratedLinesInteger" + }, + "numberOfGeneratedFiles": { + "shape": "DocV2GenerationEventNumberOfGeneratedFilesInteger" + }, + "interactionType": { + "shape": "DocInteractionType" + }, + "numberOfNavigations": { + "shape": "DocV2GenerationEventNumberOfNavigationsInteger" + }, + "folderLevel": { + "shape": "DocFolderLevel" + } + } + }, + "DocV2GenerationEventNumberOfGeneratedCharsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfGeneratedFilesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfGeneratedLinesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfNavigationsInteger": { + "type": "integer", + "min": 0 + }, "DocumentSymbol": { "type": "structure", "required": ["name", "type"], @@ -1528,12 +1747,18 @@ "EnvState": { "type": "structure", "members": { - "operatingSystem": { "shape": "EnvStateOperatingSystemString" }, + "operatingSystem": { + "shape": "EnvStateOperatingSystemString" + }, "currentWorkingDirectory": { "shape": "EnvStateCurrentWorkingDirectoryString" }, - "environmentVariables": { "shape": "EnvironmentVariables" }, - "timezoneOffset": { "shape": "EnvStateTimezoneOffsetInteger" } + "environmentVariables": { + "shape": "EnvironmentVariables" + }, + "timezoneOffset": { + "shape": "EnvStateTimezoneOffsetInteger" + } } }, "EnvStateCurrentWorkingDirectoryString": { @@ -1589,14 +1814,18 @@ "type": "structure", "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], "members": { - "conversationId": { "shape": "ConversationId" }, + "conversationId": { + "shape": "ConversationId" + }, "linesOfCodeAccepted": { "shape": "FeatureDevCodeAcceptanceEventLinesOfCodeAcceptedInteger" }, "charactersOfCodeAccepted": { "shape": "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger" }, - "programmingLanguage": { "shape": "ProgrammingLanguage" } + "programmingLanguage": { + "shape": "ProgrammingLanguage" + } } }, "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger": { @@ -1611,7 +1840,9 @@ "type": "structure", "required": ["conversationId", "linesOfCodeGenerated", "charactersOfCodeGenerated"], "members": { - "conversationId": { "shape": "ConversationId" }, + "conversationId": { + "shape": "ConversationId" + }, "linesOfCodeGenerated": { "shape": "FeatureDevCodeGenerationEventLinesOfCodeGeneratedInteger" }, @@ -1756,19 +1987,33 @@ "type": "structure", "required": ["fileContext"], "members": { - "fileContext": { "shape": "FileContext" }, + "fileContext": { + "shape": "FileContext" + }, "maxResults": { "shape": "GenerateCompletionsRequestMaxResultsInteger" }, - "nextToken": { "shape": "GenerateCompletionsRequestNextTokenString" }, + "nextToken": { + "shape": "GenerateCompletionsRequestNextTokenString" + }, "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" }, - "supplementalContexts": { "shape": "SupplementalContextList" }, - "customizationArn": { "shape": "CustomizationArn" }, - "optOutPreference": { "shape": "OptOutPreference" }, - "userContext": { "shape": "UserContext" }, - "profileArn": { "shape": "ProfileArn" } + "supplementalContexts": { + "shape": "SupplementalContextList" + }, + "customizationArn": { + "shape": "CustomizationArn" + }, + "optOutPreference": { + "shape": "OptOutPreference" + }, + "userContext": { + "shape": "UserContext" + }, + "profileArn": { + "shape": "ProfileArn" + } } }, "GenerateCompletionsRequestMaxResultsInteger": { @@ -1825,7 +2070,9 @@ "type": "structure", "required": ["jobId"], "members": { - "jobId": { "shape": "GetCodeFixJobRequestJobIdString" } + "jobId": { + "shape": "GetCodeFixJobRequestJobIdString" + } } }, "GetCodeFixJobRequestJobIdString": { @@ -2073,7 +2320,9 @@ "maxResults": { "shape": "ListAvailableCustomizationsRequestMaxResultsInteger" }, - "nextToken": { "shape": "Base64EncodedPaginationToken" } + "nextToken": { + "shape": "Base64EncodedPaginationToken" + } } }, "ListAvailableCustomizationsRequestMaxResultsInteger": { @@ -2330,9 +2579,15 @@ "relativeFilePath": { "shape": "RelevantTextDocumentRelativeFilePathString" }, - "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "text": { "shape": "RelevantTextDocumentTextString" }, - "documentSymbols": { "shape": "DocumentSymbols" } + "programmingLanguage": { + "shape": "ProgrammingLanguage" + }, + "text": { + "shape": "RelevantTextDocumentTextString" + }, + "documentSymbols": { + "shape": "DocumentSymbols" + } } }, "RelevantTextDocumentRelativeFilePathString": { @@ -2635,7 +2890,7 @@ }, "StartCodeFixJobRequestDescriptionString": { "type": "string", - "max": 2000, + "max": 5000, "min": 1, "sensitive": true }, @@ -2786,9 +3041,7 @@ } } }, - "String": { - "type": "string" - }, + "String": { "type": "string" }, "SuggestedFix": { "type": "structure", "members": { @@ -2985,35 +3238,75 @@ "TelemetryEvent": { "type": "structure", "members": { - "userTriggerDecisionEvent": { "shape": "UserTriggerDecisionEvent" }, - "codeCoverageEvent": { "shape": "CodeCoverageEvent" }, - "userModificationEvent": { "shape": "UserModificationEvent" }, - "codeScanEvent": { "shape": "CodeScanEvent" }, - "codeScanSucceededEvent": { "shape": "CodeScanSucceededEvent" }, - "codeScanFailedEvent": { "shape": "CodeScanFailedEvent" }, - "codeScanRemediationsEvent": { "shape": "CodeScanRemediationsEvent" }, - "codeFixGenerationEvent": { "shape": "CodeFixGenerationEvent" }, - "codeFixAcceptanceEvent": { "shape": "CodeFixAcceptanceEvent" }, - "metricData": { "shape": "MetricData" }, - "chatAddMessageEvent": { "shape": "ChatAddMessageEvent" }, + "userTriggerDecisionEvent": { + "shape": "UserTriggerDecisionEvent" + }, + "codeCoverageEvent": { + "shape": "CodeCoverageEvent" + }, + "userModificationEvent": { + "shape": "UserModificationEvent" + }, + "codeScanEvent": { + "shape": "CodeScanEvent" + }, + "codeScanSucceededEvent": { + "shape": "CodeScanSucceededEvent" + }, + "codeScanFailedEvent": { + "shape": "CodeScanFailedEvent" + }, + "codeScanRemediationsEvent": { + "shape": "CodeScanRemediationsEvent" + }, + "codeFixGenerationEvent": { + "shape": "CodeFixGenerationEvent" + }, + "codeFixAcceptanceEvent": { + "shape": "CodeFixAcceptanceEvent" + }, + "metricData": { + "shape": "MetricData" + }, + "chatAddMessageEvent": { + "shape": "ChatAddMessageEvent" + }, "chatInteractWithMessageEvent": { "shape": "ChatInteractWithMessageEvent" }, - "chatUserModificationEvent": { "shape": "ChatUserModificationEvent" }, + "chatUserModificationEvent": { + "shape": "ChatUserModificationEvent" + }, "terminalUserInteractionEvent": { "shape": "TerminalUserInteractionEvent" }, - "featureDevEvent": { "shape": "FeatureDevEvent" }, + "featureDevEvent": { + "shape": "FeatureDevEvent" + }, "featureDevCodeGenerationEvent": { "shape": "FeatureDevCodeGenerationEvent" }, "featureDevCodeAcceptanceEvent": { "shape": "FeatureDevCodeAcceptanceEvent" }, - "inlineChatEvent": { "shape": "InlineChatEvent" }, - "transformEvent": { "shape": "TransformEvent" }, - "docGenerationEvent": { "shape": "DocGenerationEvent" }, - "testGenerationEvent": { "shape": "TestGenerationEvent" } + "inlineChatEvent": { + "shape": "InlineChatEvent" + }, + "transformEvent": { + "shape": "TransformEvent" + }, + "docGenerationEvent": { + "shape": "DocGenerationEvent" + }, + "docV2GenerationEvent": { + "shape": "DocV2GenerationEvent" + }, + "docV2AcceptanceEvent": { + "shape": "DocV2AcceptanceEvent" + }, + "testGenerationEvent": { + "shape": "TestGenerationEvent" + } }, "union": true }, @@ -3023,14 +3316,30 @@ "terminalUserInteractionEventType": { "shape": "TerminalUserInteractionEventType" }, - "terminal": { "shape": "String" }, - "terminalVersion": { "shape": "String" }, - "shell": { "shape": "String" }, - "shellVersion": { "shape": "String" }, - "duration": { "shape": "Integer" }, - "timeToSuggestion": { "shape": "Integer" }, - "isCompletionAccepted": { "shape": "Boolean" }, - "cliToolCommand": { "shape": "String" } + "terminal": { + "shape": "String" + }, + "terminalVersion": { + "shape": "String" + }, + "shell": { + "shape": "String" + }, + "shellVersion": { + "shape": "String" + }, + "duration": { + "shape": "Integer" + }, + "timeToSuggestion": { + "shape": "Integer" + }, + "isCompletionAccepted": { + "shape": "Boolean" + }, + "cliToolCommand": { + "shape": "String" + } } }, "TerminalUserInteractionEventType": { @@ -3179,6 +3488,9 @@ "members": { "message": { "shape": "String" + }, + "reason": { + "shape": "ThrottlingExceptionReason" } }, "exception": true, @@ -3186,6 +3498,10 @@ "throttling": true } }, + "ThrottlingExceptionReason": { + "type": "string", + "enum": ["MONTHLY_REQUEST_COUNT"] + }, "Timestamp": { "type": "timestamp" }, @@ -3226,7 +3542,9 @@ "downloadArtifactType": { "shape": "TransformationDownloadArtifactType" }, - "downloadArtifactId": { "shape": "ArtifactId" } + "downloadArtifactId": { + "shape": "ArtifactId" + } } }, "TransformationDownloadArtifactType": { @@ -3243,7 +3561,7 @@ }, "TransformationJavaRuntimeEnv": { "type": "string", - "enum": ["JVM_8", "JVM_11", "JVM_17"] + "enum": ["JVM_8", "JVM_11", "JVM_17", "JVM_21"] }, "TransformationJob": { "type": "structure", @@ -3278,7 +3596,7 @@ }, "TransformationLanguage": { "type": "string", - "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP", "COBOL", "PL_I", "JCL"] + "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "JAVA_21", "C_SHARP", "COBOL", "PL_I", "JCL"] }, "TransformationLanguages": { "type": "list", @@ -3351,9 +3669,15 @@ "TransformationProjectState": { "type": "structure", "members": { - "language": { "shape": "TransformationLanguage" }, - "runtimeEnv": { "shape": "TransformationRuntimeEnv" }, - "platformConfig": { "shape": "TransformationPlatformConfig" }, + "language": { + "shape": "TransformationLanguage" + }, + "runtimeEnv": { + "shape": "TransformationRuntimeEnv" + }, + "platformConfig": { + "shape": "TransformationPlatformConfig" + }, "projectArtifact": { "shape": "TransformationProjectArtifactDescriptor" } @@ -3496,8 +3820,12 @@ "transformationUploadContext": { "shape": "TransformationUploadContext" }, - "codeAnalysisUploadContext": { "shape": "CodeAnalysisUploadContext" }, - "codeFixUploadContext": { "shape": "CodeFixUploadContext" } + "codeAnalysisUploadContext": { + "shape": "CodeAnalysisUploadContext" + }, + "codeFixUploadContext": { + "shape": "CodeFixUploadContext" + } }, "union": true }, diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts index ceef0b616e7..01be84828b3 100644 --- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts +++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts @@ -25,6 +25,7 @@ import { createCodeWhispererChatStreamingClient } from '../../shared/clients/cod import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util' import { extensionVersion } from '../../shared/vscode/env' import apiConfig = require('./codewhispererruntime-2022-11-11.json') +import { UserWrittenCodeTracker } from '../../codewhisperer' import { FeatureDevCodeAcceptanceEvent, FeatureDevCodeGenerationEvent, @@ -260,6 +261,7 @@ export class FeatureDevClient { references?: CodeReference[] } } + UserWrittenCodeTracker.instance.onQFeatureInvoked() const newFileContents: { zipFilePath: string; fileContent: string }[] = [] for (const [filePath, fileContent] of Object.entries(newFiles)) { diff --git a/packages/core/src/amazonqFeatureDev/constants.ts b/packages/core/src/amazonqFeatureDev/constants.ts index 72d490e7ec4..5c00e8d7bfa 100644 --- a/packages/core/src/amazonqFeatureDev/constants.ts +++ b/packages/core/src/amazonqFeatureDev/constants.ts @@ -14,6 +14,9 @@ export const featureDevChat = 'featureDevChat' export const featureName = 'Amazon Q Developer Agent for software development' +export const generateDevFilePrompt = + "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. also you can use ”public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: install exec: component: dev commandLine: ”npm install” - id: build exec: component: dev commandLine: ”npm run build” - id: test exec: component: dev commandLine: ”npm run test”" + // Max allowed size for file collection export const maxRepoSizeBytes = 200 * 1024 * 1024 diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index f24ddb4e923..6ce82a1a0ab 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MynahIcons } from '@aws/mynah-ui' +import { ChatItemAction, MynahIcons } from '@aws/mynah-ui' import * as path from 'path' import * as vscode from 'vscode' import { EventEmitter } from 'vscode' @@ -29,7 +29,7 @@ import { } from '../../errors' import { codeGenRetryLimit, defaultRetryLimit } from '../../limits' import { Session } from '../../session/session' -import { featureDevScheme, featureName } from '../../constants' +import { featureDevScheme, featureName, generateDevFilePrompt } from '../../constants' import { DeletedFileInfo, DevPhase, MetricDataOperationName, MetricDataResult, type NewFileInfo } from '../../types' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { AuthController } from '../../../amazonq/auth/controller' @@ -38,19 +38,18 @@ import { submitFeedback } from '../../../feedback/vue/submitFeedback' import { placeholder } from '../../../shared/vscode/commands2' import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { getPathsFromZipFilePath } from '../../util/files' +import { checkForDevFile, getPathsFromZipFilePath } from '../../util/files' import { examples, messageWithConversationId } from '../../userFacingText' import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils' import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' import { i18n } from '../../../shared/i18n-helper' import globals from '../../../shared/extensionGlobals' +import { CodeWhispererSettings } from '../../../codewhisperer' import { randomUUID } from '../../../shared' import { FollowUpTypes } from '../../../amazonq/commons/types' import { Messenger } from '../../../amazonq/commons/connector/baseMessenger' import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage' -export const TotalSteps = 3 - export interface ChatControllerEventEmitters { readonly processHumanChatMessage: EventEmitter readonly followUpClicked: EventEmitter @@ -157,6 +156,17 @@ export class FeatureDevController { case FollowUpTypes.SendFeedback: this.sendFeedback() break + case FollowUpTypes.AcceptAutoBuild: + return this.processAutoBuildSetting(true, data) + case FollowUpTypes.DenyAutoBuild: + return this.processAutoBuildSetting(false, data) + case FollowUpTypes.GenerateDevFile: + this.messenger.sendAnswer({ + type: 'system-prompt', + tabID: data?.tabID, + message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), + }) + return this.newTask(data, generateDevFilePrompt) } }) this.chatControllerMessageListeners.openDiff.event((data) => { @@ -371,6 +381,9 @@ export class FeatureDevController { getLogger().debug(`${featureName}: Processing message: ${message.message}`) session = await this.sessionStorage.getSession(message.tabID) + // set latestMessage in session as retry would lose context if function returns early + session.latestMessage = message.message + await session.disableFileList() const authState = await AuthUtil.instance.getChatAuthState() if (authState.amazonQ !== 'connected') { @@ -379,7 +392,17 @@ export class FeatureDevController { return } - await session.preloader(message.message) + const root = session.getWorkspaceRoot() + const autoBuildProjectSetting = CodeWhispererSettings.instance.getAutoBuildSetting() + const hasDevfile = await checkForDevFile(root) + const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root) + + if (hasDevfile && !isPromptedForAutoBuildFeature) { + await this.promptAllowQCommandsConsent(message.tabID) + return + } + + await session.preloader() if (session.state.phase === DevPhase.CODEGEN) { await this.onCodeGeneration(session, message.message, message.tabID) @@ -392,6 +415,32 @@ export class FeatureDevController { } } + private async promptAllowQCommandsConsent(tabID: string) { + this.messenger.sendAnswer({ + tabID: tabID, + message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'), + type: 'answer', + }) + + this.messenger.sendAnswer({ + message: undefined, + type: 'system-prompt', + followUps: [ + { + pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'), + type: FollowUpTypes.AcceptAutoBuild, + status: 'success', + }, + { + pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'), + type: FollowUpTypes.DenyAutoBuild, + status: 'error', + }, + ], + tabID: tabID, + }) + } + /** * Handle a regular incoming message when a user is in the code generation phase */ @@ -462,12 +511,17 @@ export class FeatureDevController { if (remainingIterations !== undefined && totalIterations !== undefined) { this.messenger.sendAnswer({ - type: 'answer', + type: 'answer' as const, tabID: tabID, - message: - remainingIterations === 0 - ? 'Would you like me to add this code to your project?' - : `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.`, + message: (() => { + if (remainingIterations > 2) { + return 'Would you like me to add this code to your project, or provide feedback for new code?' + } else if (remainingIterations > 0) { + return `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.` + } else { + return 'Would you like me to add this code to your project?' + } + })(), }) } @@ -516,11 +570,10 @@ export class FeatureDevController { // Finish processing the event if (session?.state?.tokenSource?.token.isCancellationRequested) { - this.workOnNewTask( + await this.workOnNewTask( session.tabID, - session.state.codeGenerationRemainingIterationCount || - TotalSteps - (session.state?.currentIteration || 0), - session.state.codeGenerationTotalIterationCount || TotalSteps, + session.state.codeGenerationRemainingIterationCount, + session.state.codeGenerationTotalIterationCount, session?.state?.tokenSource?.token.isCancellationRequested ) this.disposeToken(session) @@ -555,39 +608,63 @@ export class FeatureDevController { }) } - private workOnNewTask( + private async workOnNewTask( tabID: string, remainingIterations: number = 0, totalIterations?: number, isStoppedGeneration: boolean = false ) { + const hasDevFile = await checkForDevFile((await this.sessionStorage.getSession(tabID)).getWorkspaceRoot()) + if (isStoppedGeneration) { this.messenger.sendAnswer({ - message: - (remainingIterations ?? 0) <= 0 - ? "I stopped generating your code. You don't have more iterations left, however, you can start a new session." - : `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.`, + message: ((remainingIterations) => { + if (totalIterations !== undefined) { + if (remainingIterations <= 0) { + return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." + } else if (remainingIterations <= 2) { + return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.` + } + } + return 'I stopped generating your code. If you want to continue working on this task, provide another description.' + })(remainingIterations), type: 'answer-part', tabID, }) } if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) { + const followUps: Array = [ + { + pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), + type: FollowUpTypes.NewTask, + status: 'info', + }, + { + pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), + type: FollowUpTypes.CloseSession, + status: 'info', + }, + ] + + if (!hasDevFile) { + followUps.push({ + pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), + type: FollowUpTypes.GenerateDevFile, + status: 'info', + }) + + this.messenger.sendAnswer({ + type: 'answer', + tabID, + message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'), + }) + } + this.messenger.sendAnswer({ type: 'system-prompt', tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps, }) this.messenger.sendChatInputEnabled(tabID, false) this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) @@ -598,6 +675,20 @@ export class FeatureDevController { this.messenger.sendChatInputEnabled(tabID, true) this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')) } + + private async processAutoBuildSetting(setting: boolean, msg: any) { + const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot() + await CodeWhispererSettings.instance.updateAutoBuildSetting(root, setting) + + this.messenger.sendAnswer({ + message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'), + tabID: msg.tabID, + type: 'answer', + }) + + await this.retryRequest(msg) + } + // TODO add type private async insertCode(message: any) { let session @@ -611,9 +702,10 @@ export class FeatureDevController { this.sendAcceptCodeTelemetry(session, filesAccepted) await session.insertChanges() + if (session.acceptCodeMessageId) { this.sendUpdateCodeMessage(message.tabID) - this.workOnNewTask( + await this.workOnNewTask( message.tabID, session.state.codeGenerationRemainingIterationCount, session.state.codeGenerationTotalIterationCount @@ -812,7 +904,7 @@ export class FeatureDevController { ) if (allFilePathsAccepted && allDeletedFilePathsAccepted) { this.sendUpdateCodeMessage(tabId) - this.workOnNewTask( + await this.workOnNewTask( tabId, session.state.codeGenerationRemainingIterationCount, session.state.codeGenerationTotalIterationCount @@ -931,7 +1023,7 @@ export class FeatureDevController { this.sessionStorage.deleteSession(message.tabID) } - private async newTask(message: any) { + private async newTask(message: any, prefilledPrompt?: string) { // Old session for the tab is ending, delete it so we can create a new one for the message id const session = await this.sessionStorage.getSession(message.tabID) await session.disableFileList() @@ -945,8 +1037,12 @@ export class FeatureDevController { // Re-run the opening flow, where we check auth + create a session await this.tabOpened(message) - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) + if (prefilledPrompt) { + await this.processUserChatMessage({ ...message, message: prefilledPrompt }) + } else { + this.messenger.sendChatInputEnabled(message.tabID, true) + this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) + } } private async closeSession(message: any) { diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index 0f6d13bc0e9..a1de23d1861 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -70,9 +70,9 @@ export class Session { /** * Preload any events that have to run before a chat message can be sent */ - async preloader(msg: string) { + async preloader() { if (!this.preloaderFinished) { - await this.setupConversation(msg) + await this.setupConversation() this.preloaderFinished = true this.messenger.sendAsyncEventProgress(this.tabID, true, undefined) await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation. @@ -84,10 +84,7 @@ export class Session { * * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. */ - private async setupConversation(msg: string) { - // Store the initial message when setting up the conversation so that if it fails we can retry with this message - this._latestMessage = msg - + private async setupConversation() { await telemetry.amazonq_startConversationInvoke.run(async (span) => { this._conversationId = await this.proxyClient.createConversation() getLogger().info(logWithConversationId(this.conversationId)) @@ -115,6 +112,10 @@ export class Session { this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder) } + getWorkspaceRoot(): string { + return this.config.workspaceRoots[0] + } + private getSessionStateConfig(): Omit { return { workspaceRoots: this.config.workspaceRoots, @@ -382,6 +383,10 @@ export class Session { return this._latestMessage } + set latestMessage(msg: string) { + this._latestMessage = msg + } + get telemetry() { return this._telemetry } diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts index 705232b0536..f3cd49eb972 100644 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts @@ -175,14 +175,16 @@ abstract class CodeGenBase { codeGenerationRemainingIterationCount?: number codeGenerationTotalIterationCount?: number }> { + let codeGenerationRemainingIterationCount = undefined + let codeGenerationTotalIterationCount = undefined for ( let pollingIteration = 0; pollingIteration < this.pollCount && !this.isCancellationRequested; ++pollingIteration ) { const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount - const codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount + codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount + codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount getLogger().debug(`Codegen response: %O`, codegenResult) telemetry.setCodeGenerationResult(codegenResult.codeGenerationStatus.status) @@ -272,6 +274,8 @@ abstract class CodeGenBase { newFiles: [], deletedFiles: [], references: [], + codeGenerationRemainingIterationCount: codeGenerationRemainingIterationCount, + codeGenerationTotalIterationCount: codeGenerationTotalIterationCount, } } } @@ -345,8 +349,13 @@ export class CodeGenState extends CodeGenBase implements SessionState { this.filePaths = codeGeneration.newFiles this.deletedFiles = codeGeneration.deletedFiles this.references = codeGeneration.references + this.codeGenerationRemainingIterationCount = codeGeneration.codeGenerationRemainingIterationCount this.codeGenerationTotalIterationCount = codeGeneration.codeGenerationTotalIterationCount + this.currentIteration = + this.codeGenerationRemainingIterationCount && this.codeGenerationTotalIterationCount + ? this.codeGenerationTotalIterationCount - this.codeGenerationRemainingIterationCount + : this.currentIteration + 1 if (action.uploadHistory && !action.uploadHistory[codeGenerationId] && codeGenerationId) { action.uploadHistory[codeGenerationId] = { @@ -366,7 +375,7 @@ export class CodeGenState extends CodeGenBase implements SessionState { this.deletedFiles, this.references, this.tabID, - this.currentIteration + 1, + this.currentIteration, this.codeGenerationRemainingIterationCount, this.codeGenerationTotalIterationCount, action.uploadHistory, diff --git a/packages/core/src/amazonqFeatureDev/util/files.ts b/packages/core/src/amazonqFeatureDev/util/files.ts index 8258d7fe179..b0949edeca1 100644 --- a/packages/core/src/amazonqFeatureDev/util/files.ts +++ b/packages/core/src/amazonqFeatureDev/util/files.ts @@ -7,11 +7,9 @@ import * as vscode from 'vscode' import * as path from 'path' import { collectFiles } from '../../shared/utilities/workspaceUtils' -import AdmZip from 'adm-zip' import { ContentLengthError, PrepareRepoFailedError } from '../errors' import { getLogger } from '../../shared/logger/logger' import { maxFileSizeBytes } from '../limits' -import { createHash } from 'crypto' import { CurrentWsFolders } from '../types' import { hasCode, ToolkitError } from '../../shared/errors' import { AmazonqCreateUpload, Span, telemetry as amznTelemetry } from '../../shared/telemetry/telemetry' @@ -19,8 +17,14 @@ import { TelemetryHelper } from './telemetryHelper' import { maxRepoSizeBytes } from '../constants' import { isCodeFile } from '../../shared/filetypes' import { fs } from '../../shared' +import { CodeWhispererSettings } from '../../codewhisperer' +import { ZipStream } from '../../shared/utilities/zipStream' -const getSha256 = (file: Buffer) => createHash('sha256').update(file).digest('base64') +export async function checkForDevFile(root: string) { + const devFilePath = root + '/devfile.yaml' + const hasDevFile = await fs.existsFile(devFilePath) + return hasDevFile +} /** * given the root path of the repo it zips its files in memory and generates a checksum for it. @@ -30,10 +34,13 @@ export async function prepareRepoData( workspaceFolders: CurrentWsFolders, telemetry: TelemetryHelper, span: Span, - zip: AdmZip = new AdmZip() + zip: ZipStream = new ZipStream() ) { try { - const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes) + const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting() + const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false + // We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code) + const files = await collectFiles(repoRootPaths, workspaceFolders, true, maxRepoSizeBytes, !useAutoBuildFeature) let totalBytes = 0 const ignoredExtensionMap = new Map() @@ -50,8 +57,10 @@ export async function prepareRepoData( throw error } const isCodeFile_ = isCodeFile(file.relativeFilePath) - - if (fileSize >= maxFileSizeBytes || !isCodeFile_) { + const isDevFile = file.relativeFilePath === 'devfile.yaml' + // When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit, otherwise, exclude all non code files and gitignore files + const isNonCodeFileAndIgnored = useAutoBuildFeature ? false : !isCodeFile_ || isDevFile + if (fileSize >= maxFileSizeBytes || isNonCodeFileAndIgnored) { if (!isCodeFile_) { const re = /(?:\.([^.]+))?$/ const extensionArray = re.exec(file.relativeFilePath) @@ -65,11 +74,12 @@ export async function prepareRepoData( continue } totalBytes += fileSize - - const zipFolderPath = path.dirname(file.zipFilePath) + // Paths in zip should be POSIX compliant regardless of OS + // Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + const posixPath = file.zipFilePath.split(path.sep).join(path.posix.sep) try { - zip.addLocalFile(file.fileUri.fsPath, zipFolderPath) + zip.writeFile(file.fileUri.fsPath, posixPath) } catch (error) { if (error instanceof Error && error.message.includes('File not found')) { // No-op: Skip if file was deleted or does not exist @@ -99,11 +109,12 @@ export async function prepareRepoData( telemetry.setRepositorySize(totalBytes) span.record({ amazonqRepositorySize: totalBytes }) + const zipResult = await zip.finalize() - const zipFileBuffer = zip.toBuffer() + const zipFileBuffer = zipResult.streamBuffer.getContents() || Buffer.from('') return { zipFileBuffer, - zipFileChecksum: getSha256(zipFileBuffer), + zipFileChecksum: zipResult.hash, } } catch (error) { getLogger().debug(`featureDev: Failed to prepare repo: ${error}`) diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index bd13a2d9a95..647c5682184 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -53,11 +53,9 @@ import { import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState' import DependencyVersions from '../../models/dependencies' import { getStringHash } from '../../../shared/utilities/textUtilities' -import { getVersionData } from '../../../codewhisperer/service/transformByQ/transformMavenHandler' import AdmZip from 'adm-zip' import { AuthError } from '../../../auth/sso/server' import { - setMaven, openBuildLogFile, parseBuildFile, validateSQLMetadataFile, @@ -321,12 +319,6 @@ export class GumbyController { telemetryJavaVersion = JDKToTelemetryValue(javaVersion) as CodeTransformJavaSourceVersionsAllowed } telemetry.record({ codeTransformLocalJavaVersion: telemetryJavaVersion }) - - await setMaven() - const versionInfo = await getVersionData() - const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})` - telemetry.record({ buildSystemVersion: mavenVersionInfoMessage }) - return validProjects }) return validProjects diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index 35e234cc01a..2f3a0065838 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -20,7 +20,9 @@ import { TelemetryHelper, TestGenerationBuildStep, testGenState, + tooManyRequestErrorMessage, unitTestGenerationCancelMessage, + UserWrittenCodeTracker, } from '../../../codewhisperer' import { fs, @@ -241,71 +243,76 @@ export class TestController { // eslint-disable-next-line unicorn/no-null this.messenger.sendUpdatePromptProgress(data.tabID, null) const session = this.sessionStorage.getSession() - const isCancel = data.error.message === unitTestGenerationCancelMessage - + const isCancel = data.error.uiMessage === unitTestGenerationCancelMessage + let telemetryErrorMessage = getTelemetryReasonDesc(data.error) + if (session.stopIteration) { + telemetryErrorMessage = getTelemetryReasonDesc(data.error.uiMessage.replaceAll('```', '')) + } TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, isCancel ? 'Cancelled' : 'Failed', session.startTestGenerationRequestId, performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc(data.error), + telemetryErrorMessage, session.isCodeBlockSelected, session.artifactsUploadDuration, session.srcPayloadSize, session.srcZipFileSize ) - if (session.stopIteration) { // Error from Science - this.messenger.sendMessage(data.error.message.replaceAll('```', ''), data.tabID, 'answer') + this.messenger.sendMessage(data.error.uiMessage.replaceAll('```', ''), data.tabID, 'answer') } else { isCancel - ? this.messenger.sendMessage(data.error.message, data.tabID, 'answer') + ? this.messenger.sendMessage(data.error.uiMessage, data.tabID, 'answer') : this.sendErrorMessage(data) } await this.sessionCleanUp() return } // Client side error messages - private sendErrorMessage(data: { tabID: string; error: { code: string; message: string } }) { + private sendErrorMessage(data: { + tabID: string + error: { uiMessage: string; message: string; code: string; statusCode: string } + }) { const { error, tabID } = data + // If user reached monthly limit for builderId + if (error.code === 'CreateTestJobError') { + if (error.message.includes(CodeWhispererConstants.utgLimitReached)) { + getLogger().error('Monthly quota reached for QSDA actions.') + return this.messenger.sendMessage( + i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), + tabID, + 'answer' + ) + } + if (error.message.includes('Too many requests')) { + getLogger().error(error.message) + return this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) + } + } if (isAwsError(error)) { if (error.code === 'ThrottlingException') { - // TODO: use the explicitly modeled exception reason for quota vs throttle - if (error.message.includes(CodeWhispererConstants.utgLimitReached)) { - getLogger().error('Monthly quota reached for QSDA actions.') - return this.messenger.sendMessage( - i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - tabID, - 'answer' - ) - } else { - getLogger().error('Too many requests.') - // TODO: move to constants file - this.messenger.sendErrorMessage('Too many requests. Please wait before retrying.', tabID) - } - } else { - // other service errors: - // AccessDeniedException - should not happen because access is validated before this point in the client - // ValidationException - shouldn't happen because client should not send malformed requests - // ConflictException - should not happen because the client will maintain proper state - // InternalServerException - shouldn't happen but needs to be caught - getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage( - 'Encountered an unexpected error when generating tests. Please try again', - tabID - ) + // TODO: use the explicitly modeled exception reason for quota vs throttle{ + getLogger().error(error.message) + this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) + return } - } else { - // other unexpected errors (TODO enumerate all other failure cases) + // other service errors: + // AccessDeniedException - should not happen because access is validated before this point in the client + // ValidationException - shouldn't happen because client should not send malformed requests + // ConflictException - should not happen because the client will maintain proper state + // InternalServerException - shouldn't happen but needs to be caught getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage( - 'Encountered an unexpected error when generating tests. Please try again', - tabID - ) + this.messenger.sendErrorMessage('', tabID) + return } + // other unexpected errors (TODO enumerate all other failure cases) + getLogger().error('Other error message: %s', error.uiMessage) + this.messenger.sendErrorMessage('', tabID) } // This function handles actions if user clicked on any Button one of these cases will be executed @@ -456,7 +463,14 @@ export class TestController { unsupportedMessage = `I'm sorry, but /test only supports Python and Java
I will still generate a suggestion below.` } this.messenger.sendMessage(unsupportedMessage, tabID, 'answer') - await this.onCodeGeneration(session, message.prompt, tabID, fileName, filePath) + await this.onCodeGeneration( + session, + message.prompt, + tabID, + fileName, + filePath, + workspaceFolder !== undefined + ) } else { this.messenger.sendCapabilityCard({ tabID }) this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part') @@ -656,12 +670,14 @@ export class TestController { acceptedLines = acceptedLines < 0 ? 0 : acceptedLines acceptedChars -= originalContent.length acceptedChars = acceptedChars < 0 ? 0 : acceptedChars + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const document = await vscode.workspace.openTextDocument(absolutePath) await applyChanges( document, new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end), updatedContent ) + UserWrittenCodeTracker.instance.onQFinishesEdits() } else { await fs.writeFile(absolutePath, updatedContent) } @@ -719,9 +735,13 @@ export class TestController { // this.messenger.sendMessage('Accepted', message.tabID, 'prompt') telemetry.ui_click.emit({ elementId: 'unitTestGeneration_acceptDiff' }) + getLogger().info( + `Generated unit tests are accepted for ${session.fileLanguage ?? 'plaintext'} language with jobId: ${session.listOfTestGenerationJobId[0]}, jobGroupName: ${session.testGenerationJobGroupName}, result: Succeeded` + ) TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, 'Succeeded', session.startTestGenerationRequestId, session.latencyOfTestGeneration, @@ -739,7 +759,6 @@ export class TestController { ) await this.endSession(message, FollowUpTypes.SkipBuildAndFinish) - await this.sessionCleanUp() return if (session.listOfTestGenerationJobId.length === 1) { @@ -799,20 +818,21 @@ export class TestController { message: string, tabID: string, fileName: string, - filePath: string + filePath: string, + fileInWorkspace: boolean ) { try { // TODO: Write this entire gen response to basiccommands and call here. const editorText = await fs.readFileText(filePath) const triggerPayload = { - query: `Generate unit tests for the following part of my code: ${message}`, + query: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, codeSelection: undefined, trigger: ChatTriggerType.ChatMessage, fileText: editorText, fileLanguage: session.fileLanguage, filePath: filePath, - message: `Generate unit tests for the following part of my code: ${message}`, + message: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, matchPolicy: undefined, codeQuery: undefined, userIntent: UserIntent.GENERATE_UNIT_TESTS, @@ -821,13 +841,15 @@ export class TestController { const chatRequest = triggerPayloadToChatRequest(triggerPayload) const client = await createCodeWhispererChatStreamingClient() const response = await client.generateAssistantResponse(chatRequest) + UserWrittenCodeTracker.instance.onQFeatureInvoked() await this.messenger.sendAIResponse( response, session, tabID, randomUUID.toString(), triggerPayload, - fileName + fileName, + fileInWorkspace ) } finally { this.messenger.sendChatInputEnabled(tabID, true) @@ -838,11 +860,14 @@ export class TestController { // TODO: Check if there are more cases to endSession if yes create a enum or type for step private async endSession(data: any, step: FollowUpTypes) { + this.messenger.sendMessage('Unit test generation completed.', data.tabID, 'answer') + const session = this.sessionStorage.getSession() if (step === FollowUpTypes.RejectCode) { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, 'Succeeded', session.startTestGenerationRequestId, session.latencyOfTestGeneration, @@ -858,16 +883,12 @@ export class TestController { session.numberOfTestsGenerated, session.linesOfCodeGenerated ) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_rejectDiff' }) } await this.sessionCleanUp() - // TODO: revert 'Accepted' to 'Skip build and finish' once supported - const message = step === FollowUpTypes.RejectCode ? 'Rejected' : 'Accepted' - this.messenger.sendMessage(message, data.tabID, 'prompt') - this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') + // this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') this.messenger.sendChatInputEnabled(data.tabID, true) return } @@ -1296,14 +1317,24 @@ export class TestController { if (session.tabID) { getLogger().debug('Setting input state with tabID: %s', session.tabID) this.messenger.sendChatInputEnabled(session.tabID, true) - this.messenger.sendUpdatePlaceholder(session.tabID, '/test Generate unit tests') // TODO: Change according to the UX + this.messenger.sendUpdatePlaceholder(session.tabID, 'Enter "/" for quick actions') } getLogger().debug( 'Deleting output.log and temp result directory. testGenerationLogsDir: %s', testGenerationLogsDir ) - await fs.delete(path.join(testGenerationLogsDir, 'output.log')) - await fs.delete(this.tempResultDirPath, { recursive: true }) + const outputLogPath = path.join(testGenerationLogsDir, 'output.log') + if (await fs.existsFile(outputLogPath)) { + await fs.delete(outputLogPath) + } + if ( + await fs + .stat(this.tempResultDirPath) + .then(() => true) + .catch(() => false) + ) { + await fs.delete(this.tempResultDirPath, { recursive: true }) + } } // TODO: return build command when product approves diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 10b496b69d3..f842a6c1808 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -183,7 +183,8 @@ export class Messenger { tabID: string, triggerID: string, triggerPayload: TriggerPayload, - fileName: string + fileName: string, + fileInWorkspace: boolean ) { let message = '' let messageId = response.$metadata.requestId ?? '' @@ -277,12 +278,25 @@ export class Messenger { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, false, + fileInWorkspace, 'Cancelled', messageId, performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc(CodeWhispererConstants.unitTestGenerationCancelMessage) + getTelemetryReasonDesc( + `TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}` + ), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 'TestGenCancelled' ) - this.dispatcher.sendUpdatePromptProgress( new UpdatePromptProgressMessage(tabID, cancellingProgressField) ) @@ -291,11 +305,12 @@ export class Messenger { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, false, + fileInWorkspace, 'Succeeded', messageId, - performance.now() - session.testGenerationStartTime + performance.now() - session.testGenerationStartTime, + undefined ) - this.dispatcher.sendUpdatePromptProgress( new UpdatePromptProgressMessage(tabID, testGenCompletedField) ) diff --git a/packages/core/src/amazonqTest/error.ts b/packages/core/src/amazonqTest/error.ts new file mode 100644 index 00000000000..a6694b35863 --- /dev/null +++ b/packages/core/src/amazonqTest/error.ts @@ -0,0 +1,67 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { ToolkitError } from '../shared/errors' + +export const technicalErrorCustomerFacingMessage = + 'I am experiencing technical difficulties at the moment. Please try again in a few minutes.' +const defaultTestGenErrorMessage = 'Amazon Q encountered an error while generating tests. Try again later.' +export class TestGenError extends ToolkitError { + constructor( + error: string, + code: string, + public uiMessage: string + ) { + super(error, { code }) + } +} +export class ProjectZipError extends TestGenError { + constructor(error: string) { + super(error, 'ProjectZipError', defaultTestGenErrorMessage) + } +} +export class InvalidSourceZipError extends TestGenError { + constructor() { + super('Failed to create valid source zip', 'InvalidSourceZipError', defaultTestGenErrorMessage) + } +} +export class CreateUploadUrlError extends TestGenError { + constructor(errorMessage: string) { + super(errorMessage, 'CreateUploadUrlError', technicalErrorCustomerFacingMessage) + } +} +export class UploadTestArtifactToS3Error extends TestGenError { + constructor(error: string) { + super(error, 'UploadTestArtifactToS3Error', technicalErrorCustomerFacingMessage) + } +} +export class CreateTestJobError extends TestGenError { + constructor(error: string) { + super(error, 'CreateTestJobError', technicalErrorCustomerFacingMessage) + } +} +export class TestGenTimedOutError extends TestGenError { + constructor() { + super( + 'Test generation failed. Amazon Q timed out.', + 'TestGenTimedOutError', + technicalErrorCustomerFacingMessage + ) + } +} +export class TestGenStoppedError extends TestGenError { + constructor() { + super('Unit test generation cancelled.', 'TestGenCancelled', 'Unit test generation cancelled.') + } +} +export class TestGenFailedError extends TestGenError { + constructor(error?: string) { + super(error ?? 'Test generation failed', 'TestGenFailedError', error ?? technicalErrorCustomerFacingMessage) + } +} +export class ExportResultsArchiveError extends TestGenError { + constructor(error?: string) { + super(error ?? 'Test generation failed', 'ExportResultsArchiveError', technicalErrorCustomerFacingMessage) + } +} diff --git a/packages/core/src/auth/credentials/utils.ts b/packages/core/src/auth/credentials/utils.ts index 891e17a6486..28fa5df1c4c 100644 --- a/packages/core/src/auth/credentials/utils.ts +++ b/packages/core/src/auth/credentials/utils.ts @@ -10,7 +10,6 @@ import * as vscode from 'vscode' import { Credentials } from '@aws-sdk/types' import { authHelpUrl } from '../../shared/constants' import globals from '../../shared/extensionGlobals' -import { isCloud9 } from '../../shared/extensionUtilities' import { messages, showMessageWithCancel, showViewLogsMessage } from '../../shared/utilities/messages' import { Timeout, waitTimeout } from '../../shared/utilities/timeoutUtils' import { fromExtensionManifest } from '../../shared/settings' @@ -37,8 +36,8 @@ export function asEnvironmentVariables(credentials: Credentials): NodeJS.Process export function showLoginFailedMessage(credentialsId: string, errMsg: string): void { const getHelp = localize('AWS.generic.message.getHelp', 'Get Help...') const editCreds = messages.editCredentials(false) - // TODO: getHelp page for Cloud9. - const buttons = isCloud9() ? [editCreds] : [editCreds, getHelp] + // TODO: Any work towards web/another cloud9 -esqe IDE may need different getHelp docs: + const buttons = [editCreds, getHelp] void showViewLogsMessage( localize('AWS.message.credentials.invalid', 'Credentials "{0}" failed to connect: {1}', credentialsId, errMsg), diff --git a/packages/core/src/auth/sso/constants.ts b/packages/core/src/auth/sso/constants.ts index 4b0e781ceaa..0e6bb082d7e 100644 --- a/packages/core/src/auth/sso/constants.ts +++ b/packages/core/src/auth/sso/constants.ts @@ -11,8 +11,15 @@ export const builderIdStartUrl = 'https://view.awsapps.com/start' export const internalStartUrl = 'https://amzn.awsapps.com/start' +/** + * Doc: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtochangeURL.html + */ export const ssoUrlFormatRegex = /^(https?:\/\/(.+)\.awsapps\.com\/start|https?:\/\/identitycenter\.amazonaws\.com\/ssoins-[\da-zA-Z]{16})\/?$/ -export const ssoUrlFormatMessage = - 'URLs must start with http:// or https://. Example: https://d-xxxxxxxxxx.awsapps.com/start' +/** + * It is possible for a start url to be a completely custom url that redirects to something that matches the format + * below, so this message is only a warning. + */ +export const ssoUrlFormatMessage = 'URL possibly invalid. Expected format: https://xxxxxxxxxx.awsapps.com/start' +export const urlInvalidFormatMessage = 'URL format invalid. Expected format: https://xxxxxxxxxx.com/yyyy' diff --git a/packages/core/src/auth/sso/model.ts b/packages/core/src/auth/sso/model.ts index 2a05692148e..6cc462d8a57 100644 --- a/packages/core/src/auth/sso/model.ts +++ b/packages/core/src/auth/sso/model.ts @@ -15,7 +15,6 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils' import { ssoAuthHelpUrl } from '../../shared/constants' import { openUrl } from '../../shared/utilities/vsCodeUtils' import { ToolkitError } from '../../shared/errors' -import { isCloud9 } from '../../shared/extensionUtilities' import { builderIdStartUrl } from './constants' export interface SsoToken { @@ -116,10 +115,7 @@ export async function openSsoPortalLink(startUrl: string, authorization: Authori async function showLoginNotification() { const name = startUrl === builderIdStartUrl ? localizedText.builderId() : localizedText.iamIdentityCenterFull() - // C9 doesn't support `detail` field with modals so we need to put it all in the `title` - const title = isCloud9() - ? `Confirm Code "${authorization.userCode}" for ${name} in the browser.` - : localize('AWS.auth.loginWithBrowser.messageTitle', 'Confirm Code for {0}', name) + const title = localize('AWS.auth.loginWithBrowser.messageTitle', 'Confirm Code for {0}', name) const detail = localize( 'AWS.auth.loginWithBrowser.messageDetail', 'Confirm this code in the browser: {0}', diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index d160ce4b490..dd008a55fb4 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -20,7 +20,7 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { createInputBox } from '../shared/ui/inputPrompter' import { CredentialSourceId, telemetry } from '../shared/telemetry/telemetry' import { createCommonButtons, createExitButton, createHelpButton, createRefreshButton } from '../shared/ui/buttons' -import { getIdeProperties, isAmazonQ, isCloud9 } from '../shared/extensionUtilities' +import { getIdeProperties, isAmazonQ } from '../shared/extensionUtilities' import { addScopes, getDependentAuths } from './secondaryAuth' import { DevSettings } from '../shared/settings' import { createRegionPrompter } from '../shared/ui/common/region' @@ -562,9 +562,9 @@ export class AuthNode implements TreeNode { if (conn !== undefined && conn.state !== 'valid') { item.iconPath = getIcon('vscode-error') if (conn.state === 'authenticating') { - this.setDescription(item, 'authenticating...') + item.description = 'authenticating...' } else { - this.setDescription(item, 'expired or invalid, click to authenticate') + item.description = 'expired or invalid, click to authenticate' item.command = { title: 'Reauthenticate', command: '_aws.toolkit.auth.reauthenticate', @@ -578,14 +578,6 @@ export class AuthNode implements TreeNode { return item } - - private setDescription(item: vscode.TreeItem, text: string) { - if (isCloud9()) { - item.tooltip = item.tooltip ?? text - } else { - item.description = text - } - } } export async function hasIamCredentials( diff --git a/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts b/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts index f6cc5e8cf18..6123e45f59a 100644 --- a/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts +++ b/packages/core/src/awsService/accessanalyzer/vue/iamPolicyChecks.ts @@ -179,7 +179,9 @@ export class IamPolicyChecksWebview extends VueWebview { documentType, inputPolicyType: policyType ? policyType : 'None', }) - this.client.config.credentials = new SharedIniFileCredentials() // We need to detect changes in the user's credentials + this.client.config.credentials = new SharedIniFileCredentials({ + profile: `${getProfileName()}`, + }) // We need to detect changes in the user's credentials this.client.validatePolicy( { policyDocument: IamPolicyChecksWebview.editedDocument, @@ -277,6 +279,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${this.region}`, '--config', `${globals.context.asAbsolutePath(defaultTerraformConfigPath)}`, + '--profile', + `${getProfileName()}`, ] await this.executeValidatePolicyCommand({ command, @@ -297,7 +301,15 @@ export class IamPolicyChecksWebview extends VueWebview { case 'CloudFormation': { if (isCloudFormationTemplate(document)) { const command = 'cfn-policy-validator' - const args = ['validate', '--template-path', `${document}`, '--region', `${this.region}`] + const args = [ + 'validate', + '--template-path', + `${document}`, + '--region', + `${this.region}`, + '--profile', + `${getProfileName()}`, + ] if (cfnParameterPath !== '') { args.push('--template-configuration-file', `${cfnParameterPath}`) } @@ -357,6 +369,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${tempFilePath}`, '--reference-policy-type', `${policyType}`, + '--profile', + `${getProfileName()}`, ] await this.executeCustomPolicyChecksCommand({ command, @@ -388,6 +402,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${tempFilePath}`, '--reference-policy-type', `${policyType}`, + '--profile', + `${getProfileName()}`, ] if (cfnParameterPath !== '') { args.push('--template-configuration-file', `${cfnParameterPath}`) @@ -448,6 +464,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${this.region}`, '--config', `${globals.context.asAbsolutePath(defaultTerraformConfigPath)}`, + '--profile', + `${getProfileName()}`, ] if (actions !== '') { args.push('--actions', `${actions}`) @@ -480,6 +498,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${document}`, '--region', `${this.region}`, + '--profile', + `${getProfileName()}`, ] if (actions !== '') { args.push('--actions', `${actions}`) @@ -525,6 +545,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${this.region}`, '--config', `${globals.context.asAbsolutePath(defaultTerraformConfigPath)}`, + '--profile', + `${getProfileName()}`, ] await this.executeCustomPolicyChecksCommand({ command, @@ -551,6 +573,8 @@ export class IamPolicyChecksWebview extends VueWebview { `${document}`, '--region', `${this.region}`, + '--profile', + `${getProfileName()}`, ] if (cfnParameterPath !== '') { args.push('--template-configuration-file', `${cfnParameterPath}`) @@ -925,6 +949,11 @@ export function isJsonPolicyLanguage(document: string) { return policyLanguageFileTypes.some((t) => document.endsWith(t)) } +export function getProfileName(): string | undefined { + // We neeed to split the name on 'profile:' to extract the correct profile name + return globals.awsContext.getCredentialProfileName()?.split('profile:')[1] +} + export class PolicyChecksError extends ToolkitError { constructor(message: string, code: PolicyChecksErrorCode) { super(message, { code }) diff --git a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts index f17bec9213a..511217481b3 100644 --- a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts +++ b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts @@ -57,7 +57,7 @@ export async function getFiles( return await vscode.workspace.findFiles(globPattern, excludePattern) } catch (error) { - getLogger().error(`Failed to get files with pattern ${pattern}:`, error) + getLogger().error(`Failed to find files with pattern ${pattern}:`, error) return [] } } diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts index 5f8c6b4a81e..d7d5e51bb51 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts @@ -73,7 +73,7 @@ export class AppNode implements TreeNode { createPlaceholderItem( localize( 'AWS.appBuilder.explorerNode.app.noResourceTree', - '[Unable to load Resource tree for this App. Update SAM template]' + '[Unable to load resource tree for this app. Ensure SAM template is correct.]' ) ), ] diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts index 913cdd067e0..e7181c0d5ba 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts @@ -10,7 +10,6 @@ import { createPlaceholderItem } from '../../../../shared/treeview/utils' import * as nls from 'vscode-nls' import { getLogger } from '../../../../shared/logger/logger' -import { FunctionConfiguration, LambdaClient, GetFunctionCommand } from '@aws-sdk/client-lambda' import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' import globals from '../../../../shared/extensionGlobals' import { defaultPartition } from '../../../../shared/regions/regionProvider' @@ -28,7 +27,6 @@ import { s3BucketType, } from '../../../../shared/cloudformation/cloudformation' import { ToolkitError } from '../../../../shared' -import { getIAMConnection } from '../../../../auth/utils' const localize = nls.loadMessageBundle() export interface DeployedResource { @@ -89,43 +87,16 @@ export async function generateDeployedNode( const defaultClient = new DefaultLambdaClient(regionCode) const lambdaNode = new LambdaNode(regionCode, defaultClient) let configuration: Lambda.FunctionConfiguration - let v3configuration - let logGroupName try { configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId)) .Configuration as Lambda.FunctionConfiguration newDeployedResource = new LambdaFunctionNode(lambdaNode, regionCode, configuration) } catch (error: any) { - getLogger().error('Error getting Lambda configuration %O', error) + getLogger().error('Error getting Lambda configuration: %O', error) throw ToolkitError.chain(error, 'Error getting Lambda configuration', { code: 'lambdaClientError', }) } - const connection = await getIAMConnection({ prompt: false }) - if (!connection || connection.type !== 'iam') { - return [ - createPlaceholderItem( - localize( - 'AWS.appBuilder.explorerNode.unavailableDeployedResource', - '[Failed to retrive deployed resource.]' - ) - ), - ] - } - const cred = await connection.getCredentials() - const v3Client = new LambdaClient({ region: regionCode, credentials: cred }) - - const v3command = new GetFunctionCommand({ FunctionName: deployedResource.PhysicalResourceId }) - try { - v3configuration = (await v3Client.send(v3command)).Configuration as FunctionConfiguration - logGroupName = v3configuration.LoggingConfig?.LogGroup - } catch { - getLogger().error('Error getting Lambda V3 configuration') - } - newDeployedResource.configuration = { - ...newDeployedResource.configuration, - logGroupName: logGroupName, - } as any break } case s3BucketType: { @@ -156,7 +127,10 @@ export async function generateDeployedNode( getLogger().info('Details are missing or are incomplete for: %O', deployedResource) return [ createPlaceholderItem( - localize('AWS.appBuilder.explorerNode.noApps', '[This resource is not yet supported.]') + localize( + 'AWS.appBuilder.explorerNode.noApps', + '[This resource is not yet supported in AppBuilder.]' + ) ), ] } @@ -166,7 +140,7 @@ export async function generateDeployedNode( createPlaceholderItem( localize( 'AWS.appBuilder.explorerNode.unavailableDeployedResource', - '[Failed to retrive deployed resource.]' + '[Failed to retrieve deployed resource. Ensure correct stack name and region are in the samconfig.toml, and that your account is connected.]' ) ), ] diff --git a/packages/core/src/awsService/appBuilder/explorer/samProject.ts b/packages/core/src/awsService/appBuilder/explorer/samProject.ts index fd571cd6be8..ce8d0c4878a 100644 --- a/packages/core/src/awsService/appBuilder/explorer/samProject.ts +++ b/packages/core/src/awsService/appBuilder/explorer/samProject.ts @@ -42,14 +42,17 @@ export async function getStackName(projectRoot: vscode.Uri): Promise { } catch (error: any) { switch (error.code) { case SamConfigErrorCode.samNoConfigFound: - getLogger().info('No stack name or region information available in samconfig.toml: %O', error) + getLogger().info('Stack name and/or region information not found in samconfig.toml: %O', error) break case SamConfigErrorCode.samConfigParseError: - getLogger().error(`Error getting stack name or region information: ${error.message}`, error) + getLogger().error( + `Error parsing stack name and/or region information from samconfig.toml: ${error.message}. Ensure the information is correct.`, + error + ) void showViewLogsMessage('Encountered an issue reading samconfig.toml') break default: - getLogger().warn(`Error getting stack name or region information: ${error.message}`, error) + getLogger().warn(`Error parsing stack name and/or region information: ${error.message}`, error) } return {} } diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts index de3dee8770d..63b116b20eb 100644 --- a/packages/core/src/awsService/appBuilder/utils.ts +++ b/packages/core/src/awsService/appBuilder/utils.ts @@ -24,14 +24,14 @@ const localize = nls.loadMessageBundle() export async function runOpenTemplate(arg?: TreeNode) { const templateUri = arg ? (arg.resource as SamAppLocation).samTemplateUri : await promptUserForTemplate() if (!templateUri || !(await fs.exists(templateUri))) { - throw new ToolkitError('No template provided', { code: 'NoTemplateProvided' }) + throw new ToolkitError('SAM Template not found, cannot open template', { code: 'NoTemplateProvided' }) } const document = await vscode.workspace.openTextDocument(templateUri) await vscode.window.showTextDocument(document) } /** - * Find and open the lambda handler with given ResoruceNode + * Find and open the lambda handler with given ResourceNode * If not found, a NoHandlerFound error will be raised * @param arg ResourceNode */ @@ -56,9 +56,12 @@ export async function runOpenHandler(arg: ResourceNode): Promise { arg.resource.resource.Runtime ) if (!handlerFile) { - throw new ToolkitError(`No handler file found with name "${arg.resource.resource.Handler}"`, { - code: 'NoHandlerFound', - }) + throw new ToolkitError( + `No handler file found with name "${arg.resource.resource.Handler}". Ensure the file exists in the expected location."`, + { + code: 'NoHandlerFound', + } + ) } await vscode.workspace.openTextDocument(handlerFile).then(async (doc) => await vscode.window.showTextDocument(doc)) } @@ -90,7 +93,7 @@ export async function getLambdaHandlerFile( ): Promise { const family = getFamily(runtime) if (!supportedRuntimeForHandler.has(family)) { - throw new ToolkitError(`Runtime ${runtime} is not supported for open handler button`, { + throw new ToolkitError(`Runtime ${runtime} is not supported for the 'Open handler' button`, { code: 'RuntimeNotSupported', }) } diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts index 04f43d61878..26760d896aa 100644 --- a/packages/core/src/awsService/appBuilder/walkthrough.ts +++ b/packages/core/src/awsService/appBuilder/walkthrough.ts @@ -148,13 +148,13 @@ export async function getTutorial( const appSelected = appMap.get(project + runtime) telemetry.record({ action: project + runtime, source: source ?? 'AppBuilderWalkthrough' }) if (!appSelected) { - throw new ToolkitError(`Tried to get template '${project}+${runtime}', but it hasn't been registered.`) + throw new ToolkitError(`Template '${project}+${runtime}' does not exist, choose another template.`) } try { await getPattern(serverlessLandOwner, serverlessLandRepo, appSelected.asset, outputDir, true) } catch (error) { - throw new ToolkitError(`Error occurred while fetching the pattern from serverlessland: ${error}`) + throw new ToolkitError(`An error occurred while fetching this pattern from Serverless Land: ${error}`) } } @@ -190,7 +190,7 @@ export async function genWalkthroughProject( 'No' ) if (choice !== 'Yes') { - throw new ToolkitError(`${defaultTemplateName} already exist`) + throw new ToolkitError(`A file named ${defaultTemplateName} already exists in this path.`) } } @@ -256,9 +256,9 @@ export async function initWalkthroughProjectCommand() { let runtimeSelected: TutorialRuntimeOptions | undefined = undefined try { if (!walkthroughSelected || !(typeof walkthroughSelected === 'string')) { - getLogger().info('exit on no walkthrough selected') + getLogger().info('No walkthrough selected - exiting') void vscode.window.showErrorMessage( - localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Please select a template first') + localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Select a template in the walkthrough.') ) return } @@ -322,7 +322,7 @@ export async function getOrUpdateOrInstallSAMCli(source: string) { } } } catch (err) { - throw ToolkitError.chain(err, 'Failed to install or detect SAM') + throw ToolkitError.chain(err, 'Failed to install or detect SAM.') } finally { telemetry.record({ source: source, toolId: 'sam-cli' }) } diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts index a2eabbe6d2d..4eb3c4bcf5b 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts @@ -93,7 +93,7 @@ export class AppRunnerNode extends AWSTreeNodeBase { } public startPollingNode(id: string): void { - this.pollingSet.start(id) + this.pollingSet.add(id) } public stopPollingNode(id: string): void { diff --git a/packages/core/src/awsService/cloudWatchLogs/activation.ts b/packages/core/src/awsService/cloudWatchLogs/activation.ts index bfcaaf1d0e2..5b543ab89ca 100644 --- a/packages/core/src/awsService/cloudWatchLogs/activation.ts +++ b/packages/core/src/awsService/cloudWatchLogs/activation.ts @@ -28,13 +28,13 @@ import { getLogger } from '../../shared/logger/logger' import { ToolkitError } from '../../shared' import { LiveTailCodeLensProvider } from './document/liveTailCodeLensProvider' +export const liveTailRegistry = LiveTailSessionRegistry.instance +export const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry) export async function activate(context: vscode.ExtensionContext, configuration: Settings): Promise { const registry = LogDataRegistry.instance - const liveTailRegistry = LiveTailSessionRegistry.instance const documentProvider = new LogDataDocumentProvider(registry) const liveTailDocumentProvider = new LiveTailDocumentProvider() - const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry) context.subscriptions.push( vscode.languages.registerCodeLensProvider( { @@ -150,7 +150,7 @@ export async function activate(context: vscode.ExtensionContext, configuration: ) } -function getFunctionLogGroupName(configuration: any) { +export function getFunctionLogGroupName(configuration: any) { const logGroupPrefix = '/aws/lambda/' - return configuration.logGroupName || logGroupPrefix + configuration.FunctionName + return configuration.LoggingConfig?.LogGroup || logGroupPrefix + configuration.FunctionName } diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/tailLogGroup.ts b/packages/core/src/awsService/cloudWatchLogs/commands/tailLogGroup.ts index c584a6147ce..210104f7a31 100644 --- a/packages/core/src/awsService/cloudWatchLogs/commands/tailLogGroup.ts +++ b/packages/core/src/awsService/cloudWatchLogs/commands/tailLogGroup.ts @@ -17,15 +17,17 @@ import { import { getLogger, globals, ToolkitError } from '../../../shared' import { uriToKey } from '../cloudWatchLogsUtils' import { LiveTailCodeLensProvider } from '../document/liveTailCodeLensProvider' +import { LogStreamFilterResponse } from '../wizard/liveTailLogStreamSubmenu' export async function tailLogGroup( registry: LiveTailSessionRegistry, source: string, codeLensProvider: LiveTailCodeLensProvider, - logData?: { regionName: string; groupName: string } + logData?: { regionName: string; groupName: string }, + logStreamFilterData?: LogStreamFilterResponse ): Promise { await telemetry.cloudwatchlogs_startLiveTail.run(async (span) => { - const wizard = new TailLogGroupWizard(logData) + const wizard = new TailLogGroupWizard(logData, logStreamFilterData) const wizardResponse = await wizard.run() if (!wizardResponse) { throw new CancellationError('user') diff --git a/packages/core/src/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.ts b/packages/core/src/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.ts index 025820df792..b9c14829dab 100644 --- a/packages/core/src/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.ts +++ b/packages/core/src/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.ts @@ -24,7 +24,7 @@ export interface TailLogGroupWizardResponse { } export class TailLogGroupWizard extends Wizard { - public constructor(logGroupInfo?: CloudWatchLogsGroupInfo) { + public constructor(logGroupInfo?: CloudWatchLogsGroupInfo, logStreamInfo?: LogStreamFilterResponse) { super({ initState: { regionLogGroupSubmenuResponse: logGroupInfo @@ -33,6 +33,7 @@ export class TailLogGroupWizard extends Wizard { region: logGroupInfo.regionName, } : undefined, + logStreamFilter: logStreamInfo ?? undefined, }, }) this.form.regionLogGroupSubmenuResponse.bindPrompter(createRegionLogGroupSubmenu) diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 854e6eacd1c..aa4115259c9 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -45,7 +45,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { if (!this.ec2InstanceNodes.has(instanceId)) { throw new Error(`Attempt to track ec2 node ${instanceId} that isn't a child`) } - this.pollingSet.start(instanceId) + this.pollingSet.add(instanceId) } public async updateChildren(): Promise { diff --git a/packages/core/src/awsService/s3/explorer/s3FileNode.ts b/packages/core/src/awsService/s3/explorer/s3FileNode.ts index 31b6429d265..1d4b3034c08 100644 --- a/packages/core/src/awsService/s3/explorer/s3FileNode.ts +++ b/packages/core/src/awsService/s3/explorer/s3FileNode.ts @@ -12,7 +12,6 @@ import { inspect } from 'util' import { S3BucketNode } from './s3BucketNode' import { S3FolderNode } from './s3FolderNode' import globals from '../../../shared/extensionGlobals' -import { isCloud9 } from '../../../shared/extensionUtilities' import { getIcon } from '../../../shared/icons' import { formatLocalized, getRelativeDate } from '../../../shared/datetime' @@ -42,13 +41,11 @@ export class S3FileNode extends AWSTreeNodeBase implements AWSResourceNode { } this.iconPath = getIcon('vscode-file') this.contextValue = 'awsS3FileNode' - this.command = !isCloud9() - ? { - command: 'aws.s3.openFile', - title: localize('AWS.command.s3.openFile', 'Open File'), - arguments: [this], - } - : undefined + this.command = { + command: 'aws.s3.openFile', + title: localize('AWS.command.s3.openFile', 'Open File'), + arguments: [this], + } } /** diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts index 5ea7295bf98..224bbadb2fb 100644 --- a/packages/core/src/awsexplorer/activation.ts +++ b/packages/core/src/awsexplorer/activation.ts @@ -110,22 +110,20 @@ export async function activate(args: { ) const amazonQViewNode: ToolView[] = [] - if (!isCloud9()) { - if ( - isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq) || - globals.globalState.get('aws.toolkit.amazonq.dismissed') - ) { - await setContext('aws.toolkit.amazonq.dismissed', true) - } - - // We should create the tree even if it's dismissed, in case the user installs Amazon Q later. - amazonQViewNode.push({ - nodes: [AmazonQNode.instance], - view: 'aws.amazonq.codewhisperer', - refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode], - }) + if ( + isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq) || + globals.globalState.get('aws.toolkit.amazonq.dismissed') + ) { + await setContext('aws.toolkit.amazonq.dismissed', true) } + // We should create the tree even if it's dismissed, in case the user installs Amazon Q later. + amazonQViewNode.push({ + nodes: [AmazonQNode.instance], + view: 'aws.amazonq.codewhisperer', + refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode], + }) + const viewNodes: ToolView[] = [ ...amazonQViewNode, ...codecatalystViewNode, diff --git a/packages/core/src/awsexplorer/regionNode.ts b/packages/core/src/awsexplorer/regionNode.ts index 5e6e0b06d52..e912ed373e0 100644 --- a/packages/core/src/awsexplorer/regionNode.ts +++ b/packages/core/src/awsexplorer/regionNode.ts @@ -20,6 +20,8 @@ import { StepFunctionsNode } from '../stepFunctions/explorer/stepFunctionsNodes' import { SsmDocumentNode } from '../ssmDocument/explorer/ssmDocumentNode' import { ResourcesNode } from '../dynamicResources/explorer/nodes/resourcesNode' import { AppRunnerNode } from '../awsService/apprunner/explorer/apprunnerNode' +import { DocumentDBNode } from '../docdb/explorer/docdbNode' +import { DefaultDocumentDBClient } from '../shared/clients/docdbClient' import { DefaultAppRunnerClient } from '../shared/clients/apprunnerClient' import { DefaultEcrClient } from '../shared/clients/ecrClient' import { DefaultRedshiftClient } from '../shared/clients/redshiftClient' @@ -30,8 +32,6 @@ import { getEcsRootNode } from '../awsService/ecs/model' import { compareTreeItems, TreeShim } from '../shared/treeview/utils' import { Ec2ParentNode } from '../awsService/ec2/explorer/ec2ParentNode' import { Ec2Client } from '../shared/clients/ec2Client' -import { isCloud9 } from '../shared/extensionUtilities' -import { Experiments } from '../shared/settings' interface ServiceNode { allRegions?: boolean @@ -59,13 +59,16 @@ const serviceCandidates: ServiceNode[] = [ serviceId: 'cloudformation', createFn: (regionCode: string) => new CloudFormationNode(regionCode), }, + { + serviceId: 'docdb', + createFn: (regionCode: string) => new DocumentDBNode(DefaultDocumentDBClient.create(regionCode)), + }, { serviceId: 'logs', createFn: (regionCode: string) => new CloudWatchLogsNode(regionCode), }, { serviceId: 'ec2', - when: () => Experiments.instance.isExperimentEnabled('ec2RemoteConnect'), createFn: (regionCode: string, partitionId: string) => new Ec2ParentNode(regionCode, partitionId, new Ec2Client(regionCode)), }, @@ -74,7 +77,6 @@ const serviceCandidates: ServiceNode[] = [ createFn: (regionCode: string) => new EcrNode(new DefaultEcrClient(regionCode)), }, { - when: () => !isCloud9(), serviceId: 'redshift', createFn: (regionCode: string) => new RedshiftNode(new DefaultRedshiftClient(regionCode)), }, diff --git a/packages/core/src/awsexplorer/toolView.ts b/packages/core/src/awsexplorer/toolView.ts index e3417f25521..99d20fbdb3a 100644 --- a/packages/core/src/awsexplorer/toolView.ts +++ b/packages/core/src/awsexplorer/toolView.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceTreeDataProvider' -import { isCloud9 } from '../shared/extensionUtilities' -import { debounce } from '../shared/utilities/functionUtils' export interface ToolView { nodes: TreeNode[] @@ -26,20 +24,8 @@ export function createToolView(viewNode: ToolView): vscode.TreeView { for (const refreshCommand of viewNode.refreshCommands ?? []) { refreshCommand(treeDataProvider) } - const view = vscode.window.createTreeView(viewNode.view, { treeDataProvider }) - // Cloud9 will only refresh when refreshing the entire tree - if (isCloud9()) { - for (const node of viewNode.nodes) { - // Refreshes are delayed to guard against excessive calls to `getTreeItem` and `getChildren` - // The 10ms delay is arbitrary. A single event loop may be good enough in many scenarios. - const refresh = debounce(() => treeDataProvider.refresh(node), 10) - node.onDidChangeTreeItem?.(() => refresh()) - node.onDidChangeChildren?.(() => refresh()) - } - } - - return view + return vscode.window.createTreeView(viewNode.view, { treeDataProvider }) } async function getChildren(roots: TreeNode[]) { diff --git a/packages/core/src/codecatalyst/activation.ts b/packages/core/src/codecatalyst/activation.ts index 3e47dd53879..db5ff9e12f0 100644 --- a/packages/core/src/codecatalyst/activation.ts +++ b/packages/core/src/codecatalyst/activation.ts @@ -16,7 +16,7 @@ import { DevEnvClient } from '../shared/clients/devenvClient' import { watchRestartingDevEnvs } from './reconnect' import { ToolkitPromptSettings } from '../shared/settings' import { dontShow } from '../shared/localizedText' -import { getIdeProperties, isCloud9 } from '../shared/extensionUtilities' +import { getIdeProperties } from '../shared/extensionUtilities' import { Commands } from '../shared/vscode/commands2' import { getCodeCatalystConfig } from '../shared/clients/codecatalystClient' import { isDevenvVscode } from './utils' @@ -78,23 +78,21 @@ export async function activate(ctx: ExtContext): Promise { }) ) - if (!isCloud9()) { - await GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then((disposable) => { - ctx.extensionContext.subscriptions.push(disposable) - }) + await GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then((disposable) => { + ctx.extensionContext.subscriptions.push(disposable) + }) - await GitExtension.instance - .registerCredentialsProvider({ - getCredentials(uri: vscode.Uri) { - if (uri.authority.endsWith(getCodeCatalystConfig().gitHostname)) { - return commands.withClient((client) => authProvider.getCredentialsForGit(client)) - } - }, - }) - .then((disposable) => ctx.extensionContext.subscriptions.push(disposable)) + await GitExtension.instance + .registerCredentialsProvider({ + getCredentials(uri: vscode.Uri) { + if (uri.authority.endsWith(getCodeCatalystConfig().gitHostname)) { + return commands.withClient((client) => authProvider.getCredentialsForGit(client)) + } + }, + }) + .then((disposable) => ctx.extensionContext.subscriptions.push(disposable)) - watchRestartingDevEnvs(ctx, authProvider) - } + watchRestartingDevEnvs(ctx, authProvider) const thisDevenv = (await getThisDevEnv(authProvider))?.unwrapOrElse((err) => { getLogger().error('codecatalyst: failed to get current Dev Enviroment: %s', err) @@ -116,9 +114,10 @@ export async function activate(ctx: ExtContext): Promise { const timeoutMin = thisDevenv.summary.inactivityTimeoutMinutes const timeout = timeoutMin === 0 ? 'never' : `${timeoutMin} min` getLogger().info('codecatalyst: Dev Environment timeout=%s, ides=%O', timeout, thisDevenv.summary.ides) - if (!isCloud9() && thisDevenv && !isDevenvVscode(thisDevenv.summary.ides)) { + if (thisDevenv && !isDevenvVscode(thisDevenv.summary.ides)) { // Prevent Toolkit from reconnecting to a "non-vscode" devenv by actively closing it. // Can happen if devenv is switched to ides="cloud9", etc. + // TODO: Is this needed without cloud9 check? void vscode.commands.executeCommand('workbench.action.remote.close') return } @@ -148,10 +147,6 @@ export async function activate(ctx: ExtContext): Promise { } async function showReadmeFileOnFirstLoad(workspaceState: vscode.ExtensionContext['workspaceState']): Promise { - if (isCloud9()) { - return - } - getLogger().debug('codecatalyst: showReadmeFileOnFirstLoad()') // Check dev env state to see if this is the first time the user has connected to a dev env const isFirstLoad = workspaceState.get('aws.codecatalyst.devEnv.isFirstLoad', true) diff --git a/packages/core/src/codecatalyst/explorer.ts b/packages/core/src/codecatalyst/explorer.ts index a2239d41d89..d20fcc6d37d 100644 --- a/packages/core/src/codecatalyst/explorer.ts +++ b/packages/core/src/codecatalyst/explorer.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode' import { DevEnvironment } from '../shared/clients/codecatalystClient' -import { isCloud9 } from '../shared/extensionUtilities' import { addColor, getIcon } from '../shared/icons' import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { Commands } from '../shared/vscode/commands2' @@ -21,7 +20,6 @@ export const learnMoreCommand = Commands.declare('aws.learnMore', () => async (d return openUrl(docsUrl) }) -// Only used in rare cases on C9 export const reauth = Commands.declare( '_aws.codecatalyst.reauthenticate', () => async (conn: SsoConnection, authProvider: CodeCatalystAuthenticationProvider) => { @@ -37,7 +35,7 @@ export const onboardCommand = Commands.declare( ) async function getLocalCommands(auth: CodeCatalystAuthenticationProvider) { - const docsUrl = isCloud9() ? codecatalyst.docs.cloud9.overview : codecatalyst.docs.vscode.overview + const docsUrl = codecatalyst.docs.overview const learnMoreNode = learnMoreCommand.build(docsUrl).asTreeNode({ label: 'Learn more about CodeCatalyst', iconPath: getIcon('vscode-question'), @@ -75,15 +73,6 @@ async function getLocalCommands(auth: CodeCatalystAuthenticationProvider) { ] } - if (isCloud9()) { - const item = reauth.build(auth.activeConnection, auth).asTreeNode({ - label: 'Failed to get the current Dev Environment. Click to try again.', - iconPath: addColor(getIcon(`vscode-error`), 'notificationsErrorIcon.foreground'), - }) - - return [item] - } - return [ CodeCatalystCommands.declared.cloneRepo.build().asTreeNode({ label: 'Clone Repository', diff --git a/packages/core/src/codecatalyst/model.ts b/packages/core/src/codecatalyst/model.ts index 768a97890ee..d49d8780cde 100644 --- a/packages/core/src/codecatalyst/model.ts +++ b/packages/core/src/codecatalyst/model.ts @@ -35,49 +35,17 @@ export type DevEnvironmentId = Pick export const connectScriptPrefix = 'codecatalyst_connect' export const docs = { - vscode: { - main: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-service'), - overview: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-overview.html' - ), - devenv: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-devenvironment.html' - ), - setup: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-setup.html' - ), - troubleshoot: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-troubleshoot.html' - ), - }, - cloud9: { - // Working with Amazon CodeCatalyst - main: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-cloud9'), - // Getting Started - overview: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-cloud9-getstarted' - ), - // Opening Dev Environment settings in AWS Cloud9 - settings: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-settings-cloud9'), - // Resuming a Dev Environment in AWS Cloud9 - devenv: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-resume-cloud9'), - // Creating a Dev Environment in AWS Cloud9 - devenvCreate: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-create-cloud9' - ), - // Stopping a Dev Environment in AWS Cloud9 - devenvStop: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-stop-cloud9'), - // Deleting a Dev Environment in AWS Cloud9 - devenvDelete: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-delete-cloud9' - ), - // Editing the repo devfile for a Dev Environment in AWS Cloud9 - devfileEdit: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-edit-devfile-cloud9' - ), - // Cloning a repository in AWS Cloud9 - cloneRepo: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-clone-cloud9'), - }, + main: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-service'), + overview: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-overview.html' + ), + devenv: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-devenvironment.html' + ), + setup: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-setup.html'), + troubleshoot: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-troubleshoot.html' + ), } as const export function getCodeCatalystSsmEnv(region: string, ssmPath: string, devenv: DevEnvironmentId): NodeJS.ProcessEnv { diff --git a/packages/core/src/codecatalyst/reconnect.ts b/packages/core/src/codecatalyst/reconnect.ts index 63a932c7d1a..ecedbd3eb04 100644 --- a/packages/core/src/codecatalyst/reconnect.ts +++ b/packages/core/src/codecatalyst/reconnect.ts @@ -180,10 +180,9 @@ async function pollDevEnvs( // Don't watch this devenv, it is already being re-opened in SSH. delete devenvs[id] } else if (!isDevenvVscode(metadata.ides)) { - // Technically vscode _can_ connect to a ideRuntime=jetbrains/cloud9 devenv, but - // we refuse to anyway so that the experience is consistent with other IDEs - // (jetbrains/cloud9) which are not capable of connecting to a devenv that lacks - // their runtime/bootstrap files. + // Technically vscode _can_ connect to a ideRuntime=jetbrains devenv, but + // we refuse to anyway so that the experience is consistent since that devenv + // is not capable of connecting to a devenv that lacks their runtime/bootstrap files. const ide = metadata.ides?.[0] const toIde = ide ? ` to "${ide.name}"` : '' progress.report({ message: `Dev Environment ${devenvName} was switched${toIde}` }) diff --git a/packages/core/src/codecatalyst/vue/configure/backend.ts b/packages/core/src/codecatalyst/vue/configure/backend.ts index e2ff928f2c6..e5dd360cac7 100644 --- a/packages/core/src/codecatalyst/vue/configure/backend.ts +++ b/packages/core/src/codecatalyst/vue/configure/backend.ts @@ -25,7 +25,6 @@ import { updateDevfileCommand } from '../../devfile' import { showViewLogsMessage } from '../../../shared/utilities/messages' import { isLongReconnect, removeReconnectionInformation, saveReconnectionInformation } from '../../reconnect' import { CodeCatalystClient, DevEnvironment } from '../../../shared/clients/codecatalystClient' -import { isCloud9 } from '../../../shared/extensionUtilities' const localize = nls.loadMessageBundle() @@ -165,7 +164,7 @@ export async function showConfigureDevEnv( activePanel ??= new Panel(ctx, client, devenv, commands) const webview = await activePanel.show({ title: localize('AWS.view.configureDevEnv.title', 'Dev Environment Settings'), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codecatalyst/vue/create/backend.ts b/packages/core/src/codecatalyst/vue/create/backend.ts index d2531b12923..bdf49419243 100644 --- a/packages/core/src/codecatalyst/vue/create/backend.ts +++ b/packages/core/src/codecatalyst/vue/create/backend.ts @@ -29,7 +29,6 @@ import { isThirdPartyRepo, } from '../../../shared/clients/codecatalystClient' import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { isCloud9 } from '../../../shared/extensionUtilities' import { telemetry } from '../../../shared/telemetry/telemetry' import { isNonNullable } from '../../../shared/utilities/tsUtils' import { createOrgPrompter, createProjectPrompter } from '../../wizards/selectResource' @@ -267,7 +266,7 @@ export async function showCreateDevEnv( const webview = await activePanel!.show({ title: localize('AWS.view.createDevEnv.title', 'Create a CodeCatalyst Dev Environment'), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codecatalyst/wizards/selectResource.ts b/packages/core/src/codecatalyst/wizards/selectResource.ts index d1c3de3328b..54e3bb20951 100644 --- a/packages/core/src/codecatalyst/wizards/selectResource.ts +++ b/packages/core/src/codecatalyst/wizards/selectResource.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import * as codecatalyst from '../../shared/clients/codecatalystClient' import { createCommonButtons, createRefreshButton } from '../../shared/ui/buttons' import { @@ -104,7 +103,7 @@ function createResourcePrompter( export function createOrgPrompter( client: codecatalyst.CodeCatalystClient ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.main : docs.vscode.main + const helpUri = docs.main return createResourcePrompter(client.listSpaces(), helpUri, { title: 'Select a CodeCatalyst Organization', placeholder: 'Search for an Organization', @@ -115,7 +114,7 @@ export function createProjectPrompter( client: codecatalyst.CodeCatalystClient, spaceName?: codecatalyst.CodeCatalystOrg['name'] ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.main : docs.vscode.main + const helpUri = docs.main const projects = spaceName ? client.listProjects({ spaceName }) : client.listResources('project') return createResourcePrompter(projects, helpUri, { @@ -129,7 +128,7 @@ export function createRepoPrompter( proj?: codecatalyst.CodeCatalystProject, thirdParty?: boolean ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.cloneRepo : docs.vscode.main + const helpUri = docs.main const repos = proj ? client.listSourceRepositories({ spaceName: proj.org.name, projectName: proj.name }, thirdParty) : client.listResources('repo', thirdParty) @@ -144,7 +143,7 @@ export function createDevEnvPrompter( client: codecatalyst.CodeCatalystClient, proj?: codecatalyst.CodeCatalystProject ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.devenv : docs.vscode.devenv + const helpUri = docs.devenv const envs = proj ? client.listDevEnvironments(proj) : client.listResources('devEnvironment') const filtered = envs.map((arr) => arr.filter((env) => isDevenvVscode(env.ides))) const isData = (obj: T | DataQuickPickItem['data']): obj is T => { diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 4ea655fc4da..17934f2fe38 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -9,7 +9,6 @@ import { getTabSizeSetting } from '../shared/utilities/editorUtilities' import { KeyStrokeHandler } from './service/keyStrokeHandler' import * as EditorContext from './util/editorContext' import * as CodeWhispererConstants from './models/constants' -import { getCompletionItems } from './service/completionProvider' import { vsCodeState, ConfigurationEntry, @@ -18,18 +17,16 @@ import { SecurityTreeViewFilterState, AggregatedCodeScanIssue, CodeScanIssue, + CodeIssueGroupingStrategyState, } from './models/model' import { invokeRecommendation } from './commands/invokeRecommendation' import { acceptSuggestion } from './commands/onInlineAcceptance' -import { resetIntelliSenseState } from './util/globalStateUtil' import { CodeWhispererSettings } from './util/codewhispererSettings' import { ExtContext } from '../shared/extensions' -import { TextEditorSelectionChangeKind } from 'vscode' import { CodeWhispererTracker } from './tracker/codewhispererTracker' import * as codewhispererClient from './client/codewhisperer' import { runtimeLanguageContext } from './util/runtimeLanguageContext' import { getLogger } from '../shared/logger' -import { isCloud9 } from '../shared/extensionUtilities' import { enableCodeSuggestions, toggleCodeSuggestions, @@ -60,6 +57,7 @@ import { ignoreAllIssues, focusIssue, showExploreAgentsView, + showCodeIssueGroupingQuickPick, } from './commands/basicCommands' import { sleep } from '../shared/utilities/timeoutUtils' import { ReferenceLogViewProvider } from './service/referenceLogViewProvider' @@ -99,6 +97,7 @@ import { SecurityIssueTreeViewProvider } from './service/securityIssueTreeViewPr import { setContext } from '../shared/vscode/setContext' import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview' import { detectCommentAboveLine } from '../shared/utilities/commentUtils' +import { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' let localize: nls.LocalizeFunc @@ -110,14 +109,6 @@ export async function activate(context: ExtContext): Promise { const auth = AuthUtil.instance auth.initCodeWhispererHooks() - /** - * Enable essential intellisense default settings for AWS C9 IDE - */ - - if (isCloud9()) { - await enableDefaultConfigCloud9() - } - // TODO: is this indirection useful? registerDeclaredCommands( context.extensionContext.subscriptions, @@ -129,7 +120,9 @@ export async function activate(context: ExtContext): Promise { * CodeWhisperer security panel */ const securityPanelViewProvider = new SecurityPanelViewProvider(context.extensionContext) - activateSecurityScan() + context.extensionContext.subscriptions.push( + vscode.window.registerWebviewViewProvider(SecurityPanelViewProvider.viewType, securityPanelViewProvider) + ) // TODO: this is already done in packages/core/src/extensionCommon.ts, why doesn't amazonq use that? registerCommandErrorHandler((info, error) => { @@ -288,6 +281,8 @@ export async function activate(context: ExtContext): Promise { listCodeWhispererCommands.register(), // quick pick with security issues tree filters showSecurityIssueFilters.register(), + // quick pick code issue grouping strategy + showCodeIssueGroupingQuickPick.register(), // reset security issue filters clearFilters.register(), // handle security issues tree item clicked @@ -296,6 +291,10 @@ export async function activate(context: ExtContext): Promise { SecurityTreeViewFilterState.instance.onDidChangeState((e) => { SecurityIssueTreeViewProvider.instance.refresh() }), + // refresh treeview when grouping strategy changes + CodeIssueGroupingStrategyState.instance.onDidChangeState((e) => { + SecurityIssueTreeViewProvider.instance.refresh() + }), // show a no match state SecurityIssueTreeViewProvider.instance.onDidChangeTreeData((e) => { const noMatches = @@ -481,22 +480,6 @@ export async function activate(context: ExtContext): Promise { }) } - function activateSecurityScan() { - context.extensionContext.subscriptions.push( - vscode.window.registerWebviewViewProvider(SecurityPanelViewProvider.viewType, securityPanelViewProvider) - ) - - context.extensionContext.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor((editor) => { - if (isCloud9()) { - if (editor) { - securityPanelViewProvider.setDecoration(editor, editor.document.uri) - } - } - }) - ) - } - function getAutoTriggerStatus(): boolean { return CodeSuggestionsState.instance.isSuggestionsEnabled() } @@ -517,9 +500,7 @@ export async function activate(context: ExtContext): Promise { } } - if (isCloud9()) { - setSubscriptionsforCloud9() - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() } @@ -552,7 +533,7 @@ export async function activate(context: ExtContext): Promise { } CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) - + UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when * 1. It is not a backspace @@ -583,80 +564,6 @@ export async function activate(context: ExtContext): Promise { ) } - function setSubscriptionsforCloud9() { - /** - * Manual trigger - */ - context.extensionContext.subscriptions.push( - vscode.languages.registerCompletionItemProvider([...CodeWhispererConstants.platformLanguageIds], { - async provideCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - token: vscode.CancellationToken, - context: vscode.CompletionContext - ) { - const completionList = new vscode.CompletionList(getCompletionItems(document, position), false) - return completionList - }, - }), - /** - * Automated trigger - */ - vscode.workspace.onDidChangeTextDocument(async (e) => { - const editor = vscode.window.activeTextEditor - if (!editor) { - return - } - if (e.document !== editor.document) { - return - } - if (!runtimeLanguageContext.isLanguageSupported(e.document)) { - return - } - /** - * CodeWhisperer security panel dynamic handling - */ - securityPanelViewProvider.disposeSecurityPanelItem(e, editor) - CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) - - if (e.contentChanges.length === 0 || vsCodeState.isCodeWhispererEditing) { - return - } - /** - * Important: Doing this sleep(10) is to make sure - * 1. this event is processed by vs code first - * 2. editor.selection.active has been successfully updated by VS Code - * Then this event can be processed by our code. - */ - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) - }), - - /** - * On intelliSense recommendation rejection, reset set intelli sense is active state - * Maintaining this variable because VS Code does not expose official intelliSense isActive API - */ - vscode.window.onDidChangeVisibleTextEditors(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }), - vscode.window.onDidChangeActiveTextEditor(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }), - vscode.window.onDidChangeTextEditorSelection(async (e) => { - if (e.kind === TextEditorSelectionChangeKind.Mouse) { - resetIntelliSenseState( - true, - getAutoTriggerStatus(), - RecommendationHandler.instance.isValidResponse() - ) - } - }), - vscode.workspace.onDidSaveTextDocument(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }) - ) - } - void FeatureConfigProvider.instance.fetchFeatureConfigs().catch((error) => { getLogger().error('Failed to fetch feature configs - %s', error) }) @@ -699,19 +606,6 @@ export async function shutdown() { await CodeWhispererTracker.getTracker().shutdown() } -export async function enableDefaultConfigCloud9() { - const editorSettings = vscode.workspace.getConfiguration('editor') - try { - await editorSettings.update('suggest.showMethods', true, vscode.ConfigurationTarget.Global) - // suggest.preview is available in vsc 1.57+ - await editorSettings.update('suggest.preview', true, vscode.ConfigurationTarget.Global) - await editorSettings.update('acceptSuggestionOnEnter', 'on', vscode.ConfigurationTarget.Global) - await editorSettings.update('snippetSuggestions', 'top', vscode.ConfigurationTarget.Global) - } catch (error) { - getLogger().error('amazonq: Failed to update user settings %O', error) - } -} - function toggleIssuesVisibility(visibleCondition: (issue: CodeScanIssue, filePath: string) => boolean) { const updatedIssues: AggregatedCodeScanIssue[] = SecurityIssueProvider.instance.issues.map((group) => ({ ...group, diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 7a869a68372..ceaf020fb02 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -17,7 +17,7 @@ import { isSsoConnection } from '../../auth/connection' import { pageableToCollection } from '../../shared/utilities/collectionUtils' import apiConfig = require('./service-2.json') import userApiConfig = require('./user-service-2.json') -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import { getLogger } from '../../shared/logger' import { indent } from '../../shared/utilities/textUtilities' import { keepAliveHeader } from './agent' @@ -133,6 +133,7 @@ export class DefaultCodeWhispererClient { } async createUserSdkClient(maxRetries?: number): Promise { + const session = CodeWhispererSessionState.instance.getSession() const isOptedOut = CodeWhispererSettings.instance.isOptoutEnabled() session.setFetchCredentialStart() const bearerToken = await AuthUtil.instance.getBearerToken() diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 123160fb0b3..1f33cb8c98c 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -626,9 +626,19 @@ "timestamp": { "shape": "Timestamp" }, "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }, "totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" }, - "totalNewCodeLineCount": { "shape": "PrimitiveInteger" } + "totalNewCodeLineCount": { "shape": "PrimitiveInteger" }, + "userWrittenCodeCharacterCount": { "shape": "CodeCoverageEventUserWrittenCodeCharacterCountInteger" }, + "userWrittenCodeLineCount": { "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" } } }, + "CodeCoverageEventUserWrittenCodeCharacterCountInteger": { + "type": "integer", + "min": 0 + }, + "CodeCoverageEventUserWrittenCodeLineCountInteger": { + "type": "integer", + "min": 0 + }, "CodeFixAcceptanceEvent": { "type": "structure", "required": ["jobId"], @@ -1016,6 +1026,11 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "DocFolderLevel": { + "type": "string", + "documentation": "

Specifies the folder depth level where the document should be generated

", + "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] + }, "DocGenerationEvent": { "type": "structure", "required": ["conversationId"], @@ -1024,27 +1039,98 @@ "numberOfAddChars": { "shape": "PrimitiveInteger" }, "numberOfAddLines": { "shape": "PrimitiveInteger" }, "numberOfAddFiles": { "shape": "PrimitiveInteger" }, - "userDecision": { "shape": "DocGenerationUserDecision" }, - "interactionType": { "shape": "DocGenerationInteractionType" }, + "userDecision": { "shape": "DocUserDecision" }, + "interactionType": { "shape": "DocInteractionType" }, "userIdentity": { "shape": "String" }, "numberOfNavigation": { "shape": "PrimitiveInteger" }, - "folderLevel": { "shape": "DocGenerationFolderLevel" } - } - }, - "DocGenerationFolderLevel": { - "type": "string", - "documentation": "

Doc Generation Folder Level

", - "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] + "folderLevel": { "shape": "DocFolderLevel" } + }, + "documentation": "

Deprecated: use DocV2AcceptanceEvent for tracking acceptance and DocV2GenerationEvent for tracking generation

" }, - "DocGenerationInteractionType": { + "DocInteractionType": { "type": "string", - "documentation": "

Doc Generation Interaction Type

", + "documentation": "

Tracks whether user chose to generate a new document, update an existing one, or edit document

", "enum": ["GENERATE_README", "UPDATE_README", "EDIT_README"] }, - "DocGenerationUserDecision": { + "DocUserDecision": { "type": "string", "enum": ["ACCEPT", "REJECT"] }, + "DocV2AcceptanceEvent": { + "type": "structure", + "required": [ + "conversationId", + "numberOfAddedChars", + "numberOfAddedLines", + "numberOfAddedFiles", + "userDecision", + "interactionType", + "numberOfNavigations", + "folderLevel" + ], + "members": { + "conversationId": { "shape": "ConversationId" }, + "numberOfAddedChars": { "shape": "DocV2AcceptanceEventNumberOfAddedCharsInteger" }, + "numberOfAddedLines": { "shape": "DocV2AcceptanceEventNumberOfAddedLinesInteger" }, + "numberOfAddedFiles": { "shape": "DocV2AcceptanceEventNumberOfAddedFilesInteger" }, + "userDecision": { "shape": "DocUserDecision" }, + "interactionType": { "shape": "DocInteractionType" }, + "numberOfNavigations": { "shape": "DocV2AcceptanceEventNumberOfNavigationsInteger" }, + "folderLevel": { "shape": "DocFolderLevel" } + }, + "documentation": "

Interaction event for /doc, emitted when user accepts or rejects the generated content

" + }, + "DocV2AcceptanceEventNumberOfAddedCharsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfAddedFilesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfAddedLinesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2AcceptanceEventNumberOfNavigationsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEvent": { + "type": "structure", + "required": [ + "conversationId", + "numberOfGeneratedChars", + "numberOfGeneratedLines", + "numberOfGeneratedFiles" + ], + "members": { + "conversationId": { "shape": "ConversationId" }, + "numberOfGeneratedChars": { "shape": "DocV2GenerationEventNumberOfGeneratedCharsInteger" }, + "numberOfGeneratedLines": { "shape": "DocV2GenerationEventNumberOfGeneratedLinesInteger" }, + "numberOfGeneratedFiles": { "shape": "DocV2GenerationEventNumberOfGeneratedFilesInteger" }, + "interactionType": { "shape": "DocInteractionType" }, + "numberOfNavigations": { "shape": "DocV2GenerationEventNumberOfNavigationsInteger" }, + "folderLevel": { "shape": "DocFolderLevel" } + }, + "documentation": "

Generation event for /doc, emitted when user requests document generation

" + }, + "DocV2GenerationEventNumberOfGeneratedCharsInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfGeneratedFilesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfGeneratedLinesInteger": { + "type": "integer", + "min": 0 + }, + "DocV2GenerationEventNumberOfNavigationsInteger": { + "type": "integer", + "min": 0 + }, "DocumentSymbol": { "type": "structure", "required": ["name", "type"], @@ -2092,7 +2178,7 @@ }, "StartCodeFixJobRequestDescriptionString": { "type": "string", - "max": 2000, + "max": 5000, "min": 1, "sensitive": true }, @@ -2419,6 +2505,8 @@ "inlineChatEvent": { "shape": "InlineChatEvent" }, "transformEvent": { "shape": "TransformEvent" }, "docGenerationEvent": { "shape": "DocGenerationEvent" }, + "docV2GenerationEvent": { "shape": "DocV2GenerationEvent" }, + "docV2AcceptanceEvent": { "shape": "DocV2AcceptanceEvent" }, "testGenerationEvent": { "shape": "TestGenerationEvent" } }, "union": true @@ -2561,12 +2649,18 @@ "type": "structure", "required": ["message"], "members": { - "message": { "shape": "String" } + "message": { "shape": "String" }, + "reason": { "shape": "ThrottlingExceptionReason" } }, "documentation": "

This exception is thrown when request was denied due to request throttling.

", "exception": true, "retryable": { "throttling": true } }, + "ThrottlingExceptionReason": { + "type": "string", + "documentation": "

Reason for ThrottlingException

", + "enum": ["MONTHLY_REQUEST_COUNT"] + }, "Timestamp": { "type": "timestamp" }, "TransformEvent": { "type": "structure", @@ -2604,7 +2698,7 @@ }, "TransformationJavaRuntimeEnv": { "type": "string", - "enum": ["JVM_8", "JVM_11", "JVM_17"] + "enum": ["JVM_8", "JVM_11", "JVM_17", "JVM_21"] }, "TransformationJob": { "type": "structure", @@ -2627,7 +2721,7 @@ }, "TransformationLanguage": { "type": "string", - "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP", "COBOL", "PL_I", "JCL"] + "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "JAVA_21", "C_SHARP", "COBOL", "PL_I", "JCL"] }, "TransformationLanguages": { "type": "list", diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index 999c2c53ac5..3693bbd4d6c 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -27,7 +27,6 @@ import { connectToEnterpriseSso, getStartUrl } from '../util/getStartUrl' import { showCodeWhispererConnectionPrompt } from '../util/showSsoPrompt' import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' import { AuthUtil } from '../util/authUtil' -import { isCloud9 } from '../../shared/extensionUtilities' import { getLogger } from '../../shared/logger' import { isExtensionActive, isExtensionInstalled, localize, openUrl } from '../../shared/utilities/vsCodeUtils' import { @@ -50,7 +49,7 @@ import { once } from '../../shared/utilities/functionUtils' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' -import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' +import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' @@ -66,7 +65,9 @@ import { cancel, confirm } from '../../shared' import { startCodeFixGeneration } from './startCodeFixGeneration' import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' +import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters' const MessageTimeOut = 5_000 @@ -105,9 +106,7 @@ export const enableCodeSuggestions = Commands.declare( await setContext('aws.codewhisperer.connected', true) await setContext('aws.codewhisperer.connectionExpired', false) vsCodeState.isFreeTierLimitReached = false - if (!isCloud9()) { - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - } + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') } ) @@ -448,9 +447,12 @@ export const applySecurityFix = Commands.declare( result: 'Succeeded', credentialStartUrl: AuthUtil.instance.startUrl, codeFixAction: 'applyFix', + autoDetected: targetIssue.autoDetected, + codewhispererCodeScanJobId: targetIssue.scanJobId, } let languageId = undefined try { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const document = await vscode.workspace.openTextDocument(targetFilePath) languageId = document.languageId const updatedContent = await getPatchedCode(targetFilePath, suggestedFix.code) @@ -565,6 +567,7 @@ export const applySecurityFix = Commands.declare( applyFixTelemetryEntry.result, !!targetIssue.suggestedFixes.length ) + UserWrittenCodeTracker.instance.onQFinishesEdits() } } ) @@ -677,7 +680,8 @@ export const generateFix = Commands.declare( }) await updateSecurityIssueWebview({ isGenerateFixLoading: true, - isGenerateFixError: false, + // eslint-disable-next-line unicorn/no-null + generateFixError: null, context: context.extensionContext, filePath: targetFilePath, shouldRefreshView: false, @@ -709,6 +713,7 @@ export const generateFix = Commands.declare( } else { hasSuggestedFix = suggestedFix !== undefined } + telemetry.record({ includesFix: hasSuggestedFix }) const updatedIssue: CodeScanIssue = { ...targetIssue, fixJobId: jobId, @@ -734,25 +739,29 @@ export const generateFix = Commands.declare( SecurityIssueProvider.instance.updateIssue(updatedIssue, targetFilePath) SecurityIssueTreeViewProvider.instance.refresh() } catch (err) { + const error = err instanceof Error ? err : new TypeError('Unexpected error') await updateSecurityIssueWebview({ issue: targetIssue, isGenerateFixLoading: false, - isGenerateFixError: true, + generateFixError: getErrorMsg(error, true), filePath: targetFilePath, context: context.extensionContext, - shouldRefreshView: true, + shouldRefreshView: false, }) SecurityIssueProvider.instance.updateIssue(targetIssue, targetFilePath) SecurityIssueTreeViewProvider.instance.refresh() throw err + } finally { + telemetry.record({ + component: targetSource, + detectorId: targetIssue.detectorId, + findingId: targetIssue.findingId, + ruleId: targetIssue.ruleId, + variant: refresh ? 'refresh' : undefined, + autoDetected: targetIssue.autoDetected, + codewhispererCodeScanJobId: targetIssue.scanJobId, + }) } - telemetry.record({ - component: targetSource, - detectorId: targetIssue.detectorId, - findingId: targetIssue.findingId, - ruleId: targetIssue.ruleId, - variant: refresh ? 'refresh' : undefined, - }) }) } ) @@ -884,6 +893,14 @@ export const showSecurityIssueFilters = Commands.declare({ id: 'aws.amazonq.secu } }) +export const showCodeIssueGroupingQuickPick = Commands.declare( + { id: 'aws.amazonq.codescan.showGroupingStrategy' }, + () => async () => { + const prompter = createCodeIssueGroupingStrategyPrompter() + await prompter.prompt() + } +) + export const focusIssue = Commands.declare( { id: 'aws.amazonq.security.focusIssue' }, () => async (issue: CodeScanIssue, filePath: string) => { diff --git a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts index 508639ac45b..e1f17e8a909 100644 --- a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts +++ b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts @@ -7,9 +7,8 @@ import * as vscode from 'vscode' import { vsCodeState, ConfigurationEntry } from '../models/model' import { resetIntelliSenseState } from '../util/globalStateUtil' import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { isCloud9 } from '../../shared/extensionUtilities' import { RecommendationHandler } from '../service/recommendationHandler' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import { RecommendationService } from '../service/recommendationService' /** @@ -21,17 +20,7 @@ export async function invokeRecommendation( client: DefaultCodeWhispererClient, config: ConfigurationEntry ) { - if (!config.isManualTriggerEnabled) { - return - } - /** - * IntelliSense in Cloud9 needs editor.suggest.showMethods - */ - if (!config.isShowMethodsEnabled && isCloud9()) { - void vscode.window.showWarningMessage('Turn on "editor.suggest.showMethods" to use Amazon Q inline suggestions') - return - } - if (!editor) { + if (!editor || !config.isManualTriggerEnabled) { return } @@ -44,6 +33,7 @@ export async function invokeRecommendation( /** * When using intelliSense, if invocation position changed, reject previous active recommendations */ + const session = CodeWhispererSessionState.instance.getSession() if (vsCodeState.isIntelliSenseActive && editor.selection.active !== session.startPos) { resetIntelliSenseState( config.isManualTriggerEnabled, diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts index 7ed36cde581..e13c197cefd 100644 --- a/packages/core/src/codewhisperer/commands/onAcceptance.ts +++ b/packages/core/src/codewhisperer/commands/onAcceptance.ts @@ -9,7 +9,6 @@ import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { CodeWhispererTracker } from '../tracker/codewhispererTracker' import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' import { getLogger } from '../../shared/logger/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { handleExtraBrackets } from '../util/closingBracketUtil' import { RecommendationHandler } from '../service/recommendationHandler' import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' @@ -30,7 +29,7 @@ export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEn path.extname(acceptanceEntry.editor.document.fileName) ) const start = acceptanceEntry.range.start - const end = isCloud9() ? acceptanceEntry.editor.selection.active : acceptanceEntry.range.end + const end = acceptanceEntry.range.end // codewhisperer will be doing editing while formatting. // formatting should not trigger consoals auto trigger @@ -45,13 +44,8 @@ export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEn } // move cursor to end of suggestion before doing code format // after formatting, the end position will still be editor.selection.active - if (!isCloud9()) { - acceptanceEntry.editor.selection = new vscode.Selection(end, end) - } + acceptanceEntry.editor.selection = new vscode.Selection(end, end) - if (isCloud9()) { - vsCodeState.isIntelliSenseActive = false - } vsCodeState.isCodeWhispererEditing = false CodeWhispererTracker.getTracker().enqueue({ time: new Date(), diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts index 3fd91d0f996..a9a888ef8c4 100644 --- a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts +++ b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts @@ -26,12 +26,13 @@ import { import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' import { ReferenceHoverProvider } from '../service/referenceHoverProvider' import { ImportAdderProvider } from '../service/importAdderProvider' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import path from 'path' import { RecommendationService } from '../service/recommendationService' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry' import { TelemetryHelper } from '../util/telemetryHelper' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' export const acceptSuggestion = Commands.declare( 'aws.amazonq.accept', @@ -88,6 +89,7 @@ export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAccept const end = acceptanceEntry.editor.selection.active vsCodeState.isCodeWhispererEditing = true + const session = CodeWhispererSessionState.instance.getSession() /** * Mitigation to right context handling mainly for auto closing bracket use case */ @@ -126,6 +128,7 @@ export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAccept acceptanceEntry.editor.document.getText(insertedCoderange), acceptanceEntry.editor.document.fileName ) + UserWrittenCodeTracker.instance.onQFinishesEdits() if (acceptanceEntry.references !== undefined) { const referenceLog = ReferenceLogViewProvider.getReferenceLog( acceptanceEntry.recommendation, @@ -140,5 +143,17 @@ export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAccept } RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) + await promoteNextSessionIfAvailable(acceptanceEntry) + } +} + +async function promoteNextSessionIfAvailable(acceptanceEntry: OnRecommendationAcceptanceEntry) { + if (acceptanceEntry.acceptIndex === 0 && acceptanceEntry.editor) { + const nextSession = CodeWhispererSessionState.instance.getNextSession() + nextSession.startPos = acceptanceEntry.editor.selection.active + CodeWhispererSessionState.instance.setSession(nextSession) + if (nextSession.recommendations.length) { + await RecommendationHandler.instance.tryShowRecommendation() + } } } diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index 698b9792187..ce91b0d6edd 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { ArtifactMap, DefaultCodeWhispererClient } from '../client/codewhisperer' -import { isCloud9 } from '../../shared/extensionUtilities' import { initSecurityScanRender } from '../service/diagnosticsProvider' import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider' import { getLogger } from '../../shared/logger' @@ -291,16 +290,7 @@ export async function startSecurityScan( scanUuid ) } else { - showSecurityScanResults( - securityPanelViewProvider, - securityRecommendationCollection, - editor, - context, - scope, - zipMetadata, - total, - scanUuid - ) + showSecurityScanResults(securityRecommendationCollection, editor, context, scope, zipMetadata, total) } TelemetryHelper.instance.sendCodeScanSucceededEvent( codeScanTelemetryEntry.codewhispererLanguage, @@ -387,28 +377,22 @@ export async function startSecurityScan( } export function showSecurityScanResults( - securityPanelViewProvider: SecurityPanelViewProvider, securityRecommendationCollection: AggregatedCodeScanIssue[], editor: vscode.TextEditor | undefined, context: vscode.ExtensionContext, scope: CodeWhispererConstants.CodeAnalysisScope, zipMetadata: ZipMetadata, - totalIssues: number, - scanUuid: string | undefined + totalIssues: number ) { - if (isCloud9()) { - securityPanelViewProvider.addLines(securityRecommendationCollection, editor) - void vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-security-panel') - } else { - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) - if ( - totalIssues > 0 && - (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT || - scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND) - ) { - SecurityIssuesTree.instance.focus() - } + initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + if ( + totalIssues > 0 && + (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT || + scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND) + ) { + SecurityIssuesTree.instance.focus() } + if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { populateCodeScanLogStream(zipMetadata.scannedFiles) } @@ -424,35 +408,32 @@ export function showScanResultsInChat( totalIssues: number, scanUuid: string | undefined ) { - if (isCloud9()) { - securityPanelViewProvider.addLines(securityRecommendationCollection, editor) - void vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-security-panel') - } else { - const tabID = ChatSessionManager.Instance.getSession().tabID - const eventData = { - message: 'Show Findings in the Chat panel', - totalIssues, - securityRecommendationCollection, - fileName: scope === CodeAnalysisScope.FILE_ON_DEMAND ? [...zipMetadata.scannedFiles][0] : undefined, - tabID, - scope, - scanUuid, - } - switch (scope) { - case CodeAnalysisScope.PROJECT: - codeScanState.getChatControllers()?.showSecurityScan.fire(eventData) - break - case CodeAnalysisScope.FILE_ON_DEMAND: - onDemandFileScanState.getChatControllers()?.showSecurityScan.fire(eventData) - break - default: - break - } - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) - if (totalIssues > 0) { - SecurityIssuesTree.instance.focus() - } + const tabID = ChatSessionManager.Instance.getSession().tabID + const eventData = { + message: 'Show Findings in the Chat panel', + totalIssues, + securityRecommendationCollection, + fileName: scope === CodeAnalysisScope.FILE_ON_DEMAND ? [...zipMetadata.scannedFiles][0] : undefined, + tabID, + scope, + scanUuid, + } + switch (scope) { + case CodeAnalysisScope.PROJECT: + codeScanState.getChatControllers()?.showSecurityScan.fire(eventData) + break + case CodeAnalysisScope.FILE_ON_DEMAND: + onDemandFileScanState.getChatControllers()?.showSecurityScan.fire(eventData) + break + default: + break } + + initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + if (totalIssues > 0) { + SecurityIssuesTree.instance.focus() + } + populateCodeScanLogStream(zipMetadata.scannedFiles) if (scope === CodeAnalysisScope.PROJECT) { showScanCompletedNotification(totalIssues, zipMetadata.scannedFiles) diff --git a/packages/core/src/codewhisperer/commands/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts index 3b0f41ab1d6..429e3585d36 100644 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startTestGeneration.ts @@ -15,13 +15,13 @@ import { throwIfCancelled, } from '../service/testGenHandler' import path from 'path' -import { testGenState } from '..' +import { testGenState } from '../models/model' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-restricted-imports import { BuildStatus } from '../../amazonqTest/chat/session/session' import { fs } from '../../shared/fs/fs' import { TestGenerationJobStatus } from '../models/constants' -import { TestGenFailedError } from '../models/errors' +import { TestGenFailedError } from '../../amazonqTest/error' import { Range } from '../client/codewhispereruserclient' // eslint-disable-next-line unicorn/no-null @@ -75,8 +75,9 @@ export async function startTestGenerationProcess( try { artifactMap = await getPresignedUrlAndUploadTestGen(zipMetadata) } finally { - if (await fs.existsFile(path.join(testGenerationLogsDir, 'output.log'))) { - await fs.delete(path.join(testGenerationLogsDir, 'output.log')) + const outputLogPath = path.join(testGenerationLogsDir, 'output.log') + if (await fs.existsFile(outputLogPath)) { + await fs.delete(outputLogPath) } await zipUtil.removeTmpFiles(zipMetadata) session.artifactsUploadDuration = performance.now() - uploadStartTime diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 235da682ec9..69b7cbcf96f 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -59,7 +59,7 @@ export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' export { RecommendationHandler } from './service/recommendationHandler' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' -export { session } from './util/codeWhispererSession' +export { CodeWhispererSessionState, CodeWhispererSession } from './util/codeWhispererSession' export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' export { getCompletionItems, getCompletionItem, getLabel } from './service/completionProvider' @@ -73,7 +73,7 @@ export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } f export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' -export { listScanResults, mapToAggregatedList } from './service/securityScanHandler' +export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' @@ -97,8 +97,10 @@ export * as supplementalContextUtil from './util/supplementalContext/supplementa export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' -export { SecurityScanError } from '../codewhisperer/models/errors' +export { SecurityScanError, SecurityScanTimedOutError } from '../codewhisperer/models/errors' export * as CodeWhispererConstants from '../codewhisperer/models/constants' export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil' export { Container } from './service/serviceContainer' export * from './util/gitUtil' +export * from './ui/prompters' +export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index f19a3cb6349..f0ff34dcb02 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -84,6 +84,7 @@ export const lineBreakWin = '\r\n' export const supplementalContextTimeoutInMs = 100 +export const supplementalContextMaxTotalLength = 20480 /** * Ux of recommendations */ @@ -97,6 +98,7 @@ export const completionDetail = 'Amazon Q' export const codewhisperer = 'Amazon Q' // use vscode languageId here / Supported languages +// TODO: Dropped Cloud9 support - do we need Cloud9-commented entries here? export const platformLanguageIds = [ 'java', 'python', @@ -257,9 +259,9 @@ export const codeScanZipExt = '.zip' export const contextTruncationTimeoutSeconds = 10 -export const codeScanJobTimeoutSeconds = 60 * 10 // 10 minutes +export const standardScanTimeoutMs = 600_000 // 10 minutes -export const codeFileScanJobTimeoutSeconds = 60 * 10 // 10 minutes +export const expressScanTimeoutMs = 60_000 export const codeFixJobTimeoutMs = 60_000 @@ -304,7 +306,9 @@ export const securityScanLanguageIds = [ 'csharp', 'go', 'ruby', - 'golang', // Cloud9 reports Go files with this language-id + // Cloud9 reports Go files with this language-id + // TODO: Dropped Cloud9 support - is this still needed? + 'golang', 'json', 'yaml', 'tf', @@ -725,6 +729,8 @@ export const noOpenProjectsFoundChatTestGenMessage = `Sorry, I couldn\'t find a export const unitTestGenerationCancelMessage = 'Unit test generation cancelled.' +export const tooManyRequestErrorMessage = 'Too many requests. Please wait before retrying.' + export const noJavaProjectsFoundChatMessage = `I couldn\'t find a project that I can upgrade. Currently, I support Java 8, Java 11, and Java 17 projects built on Maven. Make sure your project is open in the IDE. For more information, see the [Amazon Q documentation](${codeTransformPrereqDoc}).` export const linkToDocsHome = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html' @@ -860,7 +866,7 @@ export enum TestGenerationJobStatus { COMPLETED = 'COMPLETED', } -export enum ZipUseCase { +export enum FeatureUseCase { TEST_GENERATION = 'TEST_GENERATION', CODE_SCAN = 'CODE_SCAN', } diff --git a/packages/core/src/codewhisperer/models/errors.ts b/packages/core/src/codewhisperer/models/errors.ts index 3fe22f22af0..9466fede54d 100644 --- a/packages/core/src/codewhisperer/models/errors.ts +++ b/packages/core/src/codewhisperer/models/errors.ts @@ -172,3 +172,13 @@ export class CodeFixJobStoppedError extends CodeFixError { super('Code fix generation stopped by user.', 'CodeFixCancelled', defaultCodeFixErrorMessage) } } + +export class MonthlyCodeFixLimitError extends CodeFixError { + constructor() { + super( + i18n('AWS.amazonq.codefix.error.monthlyLimitReached'), + MonthlyCodeFixLimitError.name, + defaultCodeFixErrorMessage + ) + } +} diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 8e8e58d5fea..0f285f04ba8 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -20,6 +20,7 @@ import { TransformationSteps } from '../client/codewhispereruserclient' import { Messenger } from '../../amazonqGumby/chat/controller/messenger/messenger' import { TestChatControllerEventEmitters } from '../../amazonqTest/chat/controller/controller' import { ScanChatControllerEventEmitters } from '../../amazonqScan/controller' +import { localize } from '../../shared/utilities/vsCodeUtils' // unavoidable global variables interface VsCodeState { @@ -47,11 +48,11 @@ export const vsCodeState: VsCodeState = { isFreeTierLimitReached: false, } -export type UtgStrategy = 'ByName' | 'ByContent' +export type UtgStrategy = 'byName' | 'byContent' export type CrossFileStrategy = 'opentabs' | 'codemap' | 'bm25' | 'default' -export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'Empty' +export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'empty' export type PatchInfo = { name: string @@ -486,6 +487,7 @@ export interface CodeScanIssue { scanJobId: string language: string fixJobId?: string + autoDetected?: boolean } export interface AggregatedCodeScanIssue { @@ -564,6 +566,52 @@ export class SecurityTreeViewFilterState { } } +export enum CodeIssueGroupingStrategy { + Severity = 'Severity', + FileLocation = 'FileLocation', +} +const defaultCodeIssueGroupingStrategy = CodeIssueGroupingStrategy.Severity + +export const codeIssueGroupingStrategies = Object.values(CodeIssueGroupingStrategy) +export const codeIssueGroupingStrategyLabel: Record = { + [CodeIssueGroupingStrategy.Severity]: localize('AWS.amazonq.scans.severity', 'Severity'), + [CodeIssueGroupingStrategy.FileLocation]: localize('AWS.amazonq.scans.fileLocation', 'File Location'), +} + +export class CodeIssueGroupingStrategyState { + #fallback: CodeIssueGroupingStrategy + #onDidChangeState = new vscode.EventEmitter() + onDidChangeState = this.#onDidChangeState.event + + static #instance: CodeIssueGroupingStrategyState + static get instance() { + return (this.#instance ??= new this()) + } + + protected constructor(fallback: CodeIssueGroupingStrategy = defaultCodeIssueGroupingStrategy) { + this.#fallback = fallback + } + + public getState(): CodeIssueGroupingStrategy { + const state = globals.globalState.tryGet('aws.amazonq.codescan.groupingStrategy', String) + return this.isValidGroupingStrategy(state) ? state : this.#fallback + } + + public async setState(_state: unknown) { + const state = this.isValidGroupingStrategy(_state) ? _state : this.#fallback + await globals.globalState.update('aws.amazonq.codescan.groupingStrategy', state) + this.#onDidChangeState.fire(state) + } + + private isValidGroupingStrategy(strategy: unknown): strategy is CodeIssueGroupingStrategy { + return Object.values(CodeIssueGroupingStrategy).includes(strategy as CodeIssueGroupingStrategy) + } + + public reset() { + return this.setState(this.#fallback) + } +} + /** * Q - Transform */ @@ -1105,12 +1153,6 @@ export class TransformByQStoppedError extends ToolkitError { } } -export enum Cloud9AccessState { - NoAccess, - RequestedAccess, - HasAccess, -} - export interface TransformationCandidateProject { name: string path: string diff --git a/packages/core/src/codewhisperer/service/codeFixHandler.ts b/packages/core/src/codewhisperer/service/codeFixHandler.ts index 0358d8d3ed9..e260f3808ea 100644 --- a/packages/core/src/codewhisperer/service/codeFixHandler.ts +++ b/packages/core/src/codewhisperer/service/codeFixHandler.ts @@ -6,13 +6,14 @@ import { CodeWhispererUserClient } from '../indexNode' import * as CodeWhispererConstants from '../models/constants' import { codeFixState } from '../models/model' -import { getLogger, sleep } from '../../shared' +import { getLogger, isAwsError, sleep } from '../../shared' import { ArtifactMap, CreateUploadUrlRequest, DefaultCodeWhispererClient } from '../client/codewhisperer' import { CodeFixJobStoppedError, CodeFixJobTimedOutError, CreateCodeFixError, CreateUploadUrlError, + MonthlyCodeFixLimitError, } from '../models/errors' import { uploadArtifactToS3 } from './securityScanHandler' @@ -28,13 +29,13 @@ export async function getPresignedUrlAndUpload( } getLogger().verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { - getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err) + getLogger().error('Failed getting presigned url for uploading src context. %O', err) + throw new CreateUploadUrlError(err.message) }) getLogger().verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) getLogger().verbose(`Complete Getting presigned Url for uploading src context.`) getLogger().verbose(`Uploading src context...`) - await uploadArtifactToS3(zipFilePath, srcResp) + await uploadArtifactToS3(zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.CODE_SCAN) getLogger().verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -60,7 +61,10 @@ export async function createCodeFixJob( } const resp = await client.startCodeFixJob(req).catch((err) => { - getLogger().error(`Failed creating code fix job. Request id: ${err.requestId}`) + getLogger().error('Failed creating code fix job. %O', err) + if (isAwsError(err) && err.code === 'ThrottlingException' && err.message.includes('reached for this month')) { + throw new MonthlyCodeFixLimitError() + } throw new CreateCodeFixError() }) getLogger().info(`AmazonQ generate fix Request id: ${resp.$response.requestId}`) diff --git a/packages/core/src/codewhisperer/service/completionProvider.ts b/packages/core/src/codewhisperer/service/completionProvider.ts index 226d04dec2b..df4f2f98466 100644 --- a/packages/core/src/codewhisperer/service/completionProvider.ts +++ b/packages/core/src/codewhisperer/service/completionProvider.ts @@ -9,12 +9,13 @@ import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { Recommendation } from '../client/codewhisperer' import { LicenseUtil } from '../util/licenseUtil' import { RecommendationHandler } from './recommendationHandler' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import path from 'path' /** * completion provider for intelliSense popup */ export function getCompletionItems(document: vscode.TextDocument, position: vscode.Position) { + const session = CodeWhispererSessionState.instance.getSession() const completionItems: vscode.CompletionItem[] = [] for (const [index, recommendation] of session.recommendations.entries()) { completionItems.push(getCompletionItem(document, position, recommendation, index)) @@ -29,6 +30,7 @@ export function getCompletionItem( recommendationDetail: Recommendation, recommendationIndex: number ) { + const session = CodeWhispererSessionState.instance.getSession() const start = session.startPos const range = new vscode.Range(start, start) const recommendation = recommendationDetail.content diff --git a/packages/core/src/codewhisperer/service/importAdderProvider.ts b/packages/core/src/codewhisperer/service/importAdderProvider.ts index 717373148c4..98b7c36adfd 100644 --- a/packages/core/src/codewhisperer/service/importAdderProvider.ts +++ b/packages/core/src/codewhisperer/service/importAdderProvider.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import { Recommendation } from '../client/codewhisperer' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { findLineToInsertImportStatement } from '../util/importAdderUtil' @@ -74,8 +73,7 @@ export class ImportAdderProvider implements vscode.CodeLensProvider { private isNotEnabled(languageId: string): boolean { return ( !this.supportedLanguages.includes(languageId) || - !CodeWhispererSettings.instance.isImportRecommendationEnabled() || - isCloud9() + !CodeWhispererSettings.instance.isImportRecommendationEnabled() ) } diff --git a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts index e5ac2212e06..dedd2531901 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts @@ -5,13 +5,14 @@ import vscode, { Position } from 'vscode' import { getPrefixSuffixOverlap } from '../util/commonUtil' import { Recommendation } from '../client/codewhisperer' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import { TelemetryHelper } from '../util/telemetryHelper' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { ReferenceInlineProvider } from './referenceInlineProvider' import { ImportAdderProvider } from './importAdderProvider' import { application } from '../util/codeWhispererApplication' import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { private activeItemIndex: number | undefined @@ -20,6 +21,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt private requestId: string private startPos: Position private nextToken: string + private session = CodeWhispererSessionState.instance.getSession() private _onDidShow: vscode.EventEmitter = new vscode.EventEmitter() public readonly onDidShow: vscode.Event = this._onDidShow.event @@ -99,8 +101,8 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt const effectiveStart = document.positionAt(document.offsetAt(start) + prefix.length) const truncatedSuggestion = this.truncateOverlapWithRightContext(document, r.content, end) if (truncatedSuggestion.length === 0) { - if (session.getSuggestionState(index) !== 'Showed') { - session.setSuggestionState(index, 'Discard') + if (this.session.getSuggestionState(index) !== 'Showed') { + this.session.setSuggestionState(index, 'Discard') } return undefined } @@ -117,9 +119,9 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt index, truncatedSuggestion, this.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(index), + this.session.sessionId, + this.session.triggerType, + this.session.getCompletionType(index), runtimeLanguageContext.getLanguageContext(document.languageId, path.extname(document.fileName)) .language, r.references, @@ -154,22 +156,23 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt const end = position const iteratingIndexes = this.getIteratingIndexes() const prefix = document.getText(new vscode.Range(start, end)).replace(/\r\n/g, '\n') - const matchedCount = session.recommendations.filter( + const matchedCount = this.session.recommendations.filter( (r) => r.content.length > 0 && r.content.startsWith(prefix) && r.content !== prefix ).length for (const i of iteratingIndexes) { - const r = session.recommendations[i] + const r = this.recommendations[i] const item = this.getInlineCompletionItem(document, r, start, end, i, prefix) if (item === undefined) { continue } this.activeItemIndex = i - session.setSuggestionState(i, 'Showed') + this.session.setSuggestionState(i, 'Showed') ReferenceInlineProvider.instance.setInlineReference(this.startPos.line, r.content, r.references) ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) this.nextMove = 0 TelemetryHelper.instance.setFirstSuggestionShowTime() - session.setPerceivedLatency() + this.session.setPerceivedLatency() + UserWrittenCodeTracker.instance.onQStartsMakingEdits() this._onDidShow.fire() if (matchedCount >= 2 || this.nextToken !== '') { const result = [item] diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts index 715fd93ad2d..9cd0dda781d 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -16,7 +16,7 @@ import { shared } from '../../shared/utilities/functionUtils' import { ClassifierTrigger } from './classifierTrigger' import { getSelectedCustomization } from '../util/customizationUtil' import { codicon, getIcon } from '../../shared/icons' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import { noSuggestions } from '../models/constants' import { Commands } from '../../shared/vscode/commands2' import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' @@ -119,6 +119,7 @@ export class InlineCompletionService { errorMessage: undefined, recommendationCount: 0, } + const session = CodeWhispererSessionState.instance.getSession() try { let page = 0 while (page < this.maxPage) { @@ -127,6 +128,7 @@ export class InlineCompletionService { editor, triggerType, config, + session, autoTriggerType, true, page diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts index 623f9aaa808..d32e875e8a4 100644 --- a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts +++ b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts @@ -8,7 +8,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer' import * as CodeWhispererConstants from '../models/constants' import { ConfigurationEntry } from '../models/model' import { getLogger } from '../../shared/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { RecommendationHandler } from './recommendationHandler' import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' @@ -76,9 +75,6 @@ export class KeyStrokeHandler { } public shouldTriggerIdleTime(): boolean { - if (isCloud9() && RecommendationService.instance.isRunning) { - return false - } if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { return false } @@ -101,14 +97,6 @@ export class KeyStrokeHandler { return } - // In Cloud9, do not auto trigger when - // 1. The input is from IntelliSense acceptance event - // 2. The input is from copy and paste some code - // event.contentChanges[0].text.length > 1 is a close estimate of 1 and 2 - if (isCloud9() && event.contentChanges.length > 0 && event.contentChanges[0].text.length > 1) { - return - } - const { rightFileContent } = extractContextForCodeWhisperer(editor) const rightContextLines = rightFileContent.split(/\r?\n/) const rightContextAtCurrentLine = rightContextLines[0] @@ -259,7 +247,7 @@ export class DefaultDocumentChangedType extends DocumentChangedType { // single line && single place reformat should consist of space chars only return DocumentChangedSource.Reformatting } else { - return isCloud9() ? DocumentChangedSource.RegularKey : DocumentChangedSource.Unknown + return DocumentChangedSource.Unknown } } diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts index 72e130a5bed..92595df2fdb 100644 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -5,16 +5,20 @@ import * as vscode from 'vscode' import { extensionVersion } from '../../shared/vscode/env' -import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' +import { + RecommendationsList, + DefaultCodeWhispererClient, + CognitoCredentialsError, + ListRecommendationsRequest, +} from '../client/codewhisperer' import * as EditorContext from '../util/editorContext' import * as CodeWhispererConstants from '../models/constants' -import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' +import { CodeSuggestionsState, ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { AWSError } from 'aws-sdk' import { isAwsError } from '../../shared/errors' import { TelemetryHelper } from '../util/telemetryHelper' import { getLogger } from '../../shared/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { hasVendedIamCredentials } from '../../auth/auth' import { asyncCallWithTimeout, @@ -32,7 +36,7 @@ import { import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' import { invalidCustomizationMessage } from '../models/constants' import { getSelectedCustomization, switchToBaseCustomizationAndNotify } from '../util/customizationUtil' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState, CodeWhispererSession } from '../util/codeWhispererSession' import { Commands } from '../../shared/vscode/commands2' import globals from '../../shared/extensionGlobals' import { noSuggestions, updateInlineLockKey } from '../models/constants' @@ -44,6 +48,9 @@ import { openUrl } from '../../shared/utilities/vsCodeUtils' import { indent } from '../../shared/utilities/textUtilities' import path from 'path' import { isIamConnection } from '../../auth/connection' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' +import * as codewhispererClient from '../client/codewhisperer' +import { CodeWhispererSettings } from '../util/codewhispererSettings' /** * This class is for getRecommendation/listRecommendation API calls and its states @@ -63,9 +70,7 @@ const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => traceId: TelemetryHelper.instance.traceId, }) - if (!isCloud9('any')) { - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - } + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') RecommendationHandler.instance.reportUserDecisions(-1) await Commands.tryExecute('aws.amazonq.refreshAnnotation') }) @@ -105,6 +110,7 @@ export class RecommendationHandler { } isValidResponse(): boolean { + const session = CodeWhispererSessionState.instance.getSession() return session.recommendations.some((r) => r.content.trim() !== '') } @@ -156,14 +162,22 @@ export class RecommendationHandler { editor: vscode.TextEditor, triggerType: CodewhispererTriggerType, config: ConfigurationEntry, + session: CodeWhispererSession, autoTriggerType?: CodewhispererAutomatedTriggerType, pagination: boolean = true, page: number = 0, - generate: boolean = isIamConnection(AuthUtil.instance.conn) + generate: boolean = isIamConnection(AuthUtil.instance.conn), + isNextSession: boolean = false ): Promise { let invocationResult: 'Succeeded' | 'Failed' = 'Failed' let errorMessage: string | undefined = undefined let errorCode: string | undefined = undefined + let currentSession = session + if (isNextSession) { + getLogger().debug('pre-fetching next recommendation for model routing') + currentSession = new CodeWhispererSession() + CodeWhispererSessionState.instance.setNextSession(currentSession) + } if (!editor) { return Promise.resolve({ @@ -180,43 +194,60 @@ export class RecommendationHandler { let latency = 0 let nextToken = '' let shouldRecordServiceInvocation = true - session.language = runtimeLanguageContext.getLanguageContext( + currentSession.language = runtimeLanguageContext.getLanguageContext( editor.document.languageId, path.extname(editor.document.fileName) ).language - session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) + currentSession.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) if (pagination && !generate) { if (page === 0) { - session.requestContext = await EditorContext.buildListRecommendationRequest( - editor as vscode.TextEditor, - this.nextToken, - config.isSuggestionsWithCodeReferencesEnabled - ) + if (isNextSession) { + const request = session.requestContext.request as ListRecommendationsRequest + currentSession.requestContext = { + request: { + ...request, + fileContext: { + ...request.fileContext, + leftFileContent: `${request.fileContext.leftFileContent}${session.recommendations[0].content}`, + }, + nextToken: undefined, + }, + supplementalMetadata: currentSession.requestContext.supplementalMetadata, + } + } else { + currentSession.requestContext = await EditorContext.buildListRecommendationRequest( + editor as vscode.TextEditor, + this.nextToken, + config.isSuggestionsWithCodeReferencesEnabled + ) + } } else { - session.requestContext = { + currentSession.requestContext = { request: { - ...session.requestContext.request, + ...currentSession.requestContext.request, // Putting nextToken assignment in the end so it overwrites the existing nextToken nextToken: this.nextToken, }, - supplementalMetadata: session.requestContext.supplementalMetadata, + supplementalMetadata: currentSession.requestContext.supplementalMetadata, } } + // } } else { - session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor) + currentSession.requestContext = await EditorContext.buildGenerateRecommendationRequest( + editor as vscode.TextEditor + ) } - const request = session.requestContext.request - // record preprocessing end time + const request = currentSession.requestContext.request TelemetryHelper.instance.setPreprocessEndTime() // set start pos for non pagination call or first pagination call if (!pagination || (pagination && page === 0)) { - session.startPos = editor.selection.active - session.startCursorOffset = editor.document.offsetAt(session.startPos) - session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) - session.triggerType = triggerType - session.autoTriggerType = autoTriggerType + currentSession.startPos = editor.selection.active + currentSession.startCursorOffset = editor.document.offsetAt(currentSession.startPos) + currentSession.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, currentSession.startPos.line) + currentSession.triggerType = triggerType + currentSession.autoTriggerType = autoTriggerType /** * Validate request @@ -257,7 +288,7 @@ export class RecommendationHandler { sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] TelemetryHelper.instance.setFirstResponseRequestId(requestId) if (page === 0) { - session.setTimeToFirstRecommendation(performance.now()) + currentSession.setTimeToFirstRecommendation(performance.now()) } if (nextToken === '') { TelemetryHelper.instance.setAllPaginationEndTime() @@ -303,9 +334,9 @@ export class RecommendationHandler { vscode version: '${vscode.version}', extension version: '${extensionVersion}', filename: '${EditorContext.getFileName(editor)}', - left context of line: '${session.leftContextOfCurrentLine}', - line number: ${session.startPos.line}, - character location: ${session.startPos.character}, + left context of line: '${currentSession.leftContextOfCurrentLine}', + line number: ${currentSession.startPos.line}, + character location: ${currentSession.startPos.character}, latency: ${latency} ms. Recommendations:`, 4, @@ -313,11 +344,12 @@ export class RecommendationHandler { ).trimStart() for (const [index, item] of recommendations.entries()) { msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` - session.requestIdList.push(requestId) + currentSession.requestIdList.push(requestId) } getLogger().debug(msg) if (invocationResult === 'Succeeded') { - CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() + CodeWhispererCodeCoverageTracker.getTracker(currentSession.language)?.incrementServiceInvocationCount() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } else { if ( (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || @@ -332,6 +364,7 @@ export class RecommendationHandler { editor, triggerType, config, + currentSession, autoTriggerType, pagination, page, @@ -344,82 +377,86 @@ export class RecommendationHandler { TelemetryHelper.instance.recordServiceInvocationTelemetry( requestId, sessionId, - session.recommendations.length + recommendations.length - 1, + currentSession.recommendations.length + recommendations.length - 1, invocationResult, latency, - session.language, - session.taskType, + currentSession.language, + currentSession.taskType, reason, - session.requestContext.supplementalMetadata + currentSession.requestContext.supplementalMetadata ) } } - if (this.isCancellationRequested()) { + if (!isNextSession && this.isCancellationRequested()) { return Promise.resolve({ result: invocationResult, errorMessage: errorMessage, - recommendationCount: session.recommendations.length, + recommendationCount: currentSession.recommendations.length, }) } const typedPrefix = editor.document - .getText(new vscode.Range(session.startPos, editor.selection.active)) + .getText(new vscode.Range(currentSession.startPos, editor.selection.active)) .replace('\r\n', '\n') if (recommendations.length > 0) { TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) // mark suggestions that does not match typeahead when arrival as Discard // these suggestions can be marked as Showed if typeahead can be removed with new inline API for (const [i, r] of recommendations.entries()) { - const recommendationIndex = i + session.recommendations.length + const recommendationIndex = i + currentSession.recommendations.length if ( !r.content.startsWith(typedPrefix) && - session.getSuggestionState(recommendationIndex) === undefined + currentSession.getSuggestionState(recommendationIndex) === undefined ) { - session.setSuggestionState(recommendationIndex, 'Discard') + currentSession.setSuggestionState(recommendationIndex, 'Discard') } - session.setCompletionType(recommendationIndex, r) + currentSession.setCompletionType(recommendationIndex, r) } - session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations - if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { + currentSession.recommendations = pagination + ? currentSession.recommendations.concat(recommendations) + : recommendations + if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix, currentSession)) { this._onDidReceiveRecommendation.fire() } } this.requestId = requestId - session.sessionId = sessionId - this.nextToken = nextToken + currentSession.sessionId = sessionId + if (!isNextSession) { + this.nextToken = nextToken + } // send Empty userDecision event if user receives no recommendations in this session at all. if (invocationResult === 'Succeeded' && nextToken === '') { // case 1: empty list of suggestion [] - if (session.recommendations.length === 0) { - session.requestIdList.push(requestId) + if (currentSession.recommendations.length === 0) { + currentSession.requestIdList.push(requestId) // Received an empty list of recommendations TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList( - session.requestIdList, + currentSession.requestIdList, sessionId, page, runtimeLanguageContext.getLanguageContext( editor.document.languageId, path.extname(editor.document.fileName) ).language, - session.requestContext.supplementalMetadata + currentSession.requestContext.supplementalMetadata ) } // case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead - else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { + else if (!this.hasAtLeastOneValidSuggestion(typedPrefix, currentSession)) { this.reportUserDecisions(-1) } } return Promise.resolve({ result: invocationResult, errorMessage: errorMessage, - recommendationCount: session.recommendations.length, + recommendationCount: currentSession.recommendations.length, }) } - hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { + hasAtLeastOneValidSuggestion(typedPrefix: string, session: CodeWhispererSession): boolean { return session.recommendations.some((r) => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) } @@ -445,6 +482,7 @@ export class RecommendationHandler { * Clear recommendation state */ clearRecommendations() { + const session = CodeWhispererSessionState.instance.getSession() session.requestIdList = [] session.recommendations = [] session.suggestionStates = new Map() @@ -474,6 +512,7 @@ export class RecommendationHandler { } reportDiscardedUserDecisions() { + const session = CodeWhispererSessionState.instance.getSession() for (const [i, _] of session.recommendations.entries()) { session.setSuggestionState(i, 'Discard') } @@ -484,6 +523,7 @@ export class RecommendationHandler { * Emits telemetry reflecting user decision for current recommendation. */ reportUserDecisions(acceptIndex: number) { + const session = CodeWhispererSessionState.instance.getSession() if (session.sessionId === '' || this.requestId === '') { return } @@ -497,9 +537,7 @@ export class RecommendationHandler { session.suggestionStates, session.requestContext.supplementalMetadata ) - if (isCloud9('any')) { - this.clearRecommendations() - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { this.clearInlineCompletionStates().catch((e) => { getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) }) @@ -515,6 +553,7 @@ export class RecommendationHandler { showPrompt: boolean = false, response: GetRecommendationsResponse ): boolean { + const session = CodeWhispererSessionState.instance.getSession() const reject = () => { this.reportUserDecisions(-1) } @@ -596,6 +635,11 @@ export class RecommendationHandler { } async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { + const session = CodeWhispererSessionState.instance.getSession() + + if (!indexShift && session.recommendations.length) { + await this.fetchNextRecommendations() + } await lock.acquire(updateInlineLockKey, async () => { if (!vscode.window.state.focused) { this.reportDiscardedUserDecisions() @@ -626,7 +670,7 @@ export class RecommendationHandler { } if (noSuggestionVisible) { await vscode.commands.executeCommand(`editor.action.inlineSuggest.trigger`) - this.sendPerceivedLatencyTelemetry() + this.sendPerceivedLatencyTelemetry(session) } }) } @@ -657,7 +701,46 @@ export class RecommendationHandler { return this.inlineCompletionProvider?.getActiveItemIndex !== undefined } + async getConfigEntry(): Promise { + const codewhispererSettings = CodeWhispererSettings.instance + const isShowMethodsEnabled: boolean = + vscode.workspace.getConfiguration('editor').get('suggest.showMethods') || false + const isAutomatedTriggerEnabled: boolean = CodeSuggestionsState.instance.isSuggestionsEnabled() + const isManualTriggerEnabled: boolean = true + const isSuggestionsWithCodeReferencesEnabled = codewhispererSettings.isSuggestionsWithCodeReferencesEnabled() + + return { + isShowMethodsEnabled, + isManualTriggerEnabled, + isAutomatedTriggerEnabled, + isSuggestionsWithCodeReferencesEnabled, + } + } + + async fetchNextRecommendations() { + const session = CodeWhispererSessionState.instance.getSession() + const client = new codewhispererClient.DefaultCodeWhispererClient() + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + await this.getRecommendations( + client, + editor, + session.triggerType, + await this.getConfigEntry(), + session, + session.autoTriggerType, + true, + 0, + false, + true + ) + } + async tryShowRecommendation() { + const session = CodeWhispererSessionState.instance.getSession() const editor = vscode.window.activeTextEditor if (editor === undefined) { return @@ -686,7 +769,7 @@ export class RecommendationHandler { } } - private sendPerceivedLatencyTelemetry() { + private sendPerceivedLatencyTelemetry(session: CodeWhispererSession) { if (vscode.window.activeTextEditor) { const languageContext = runtimeLanguageContext.getLanguageContext( vscode.window.activeTextEditor.document.languageId, diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts index 0a9a4e3c034..1da76995781 100644 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -3,17 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import { isCloud9 } from '../../shared/extensionUtilities' +import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' import { isInlineCompletionEnabled } from '../util/commonUtil' import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType, telemetry, } from '../../shared/telemetry/telemetry' -import { AuthUtil } from '../util/authUtil' -import { isIamConnection } from '../../auth/connection' -import { RecommendationHandler } from '../service/recommendationHandler' import { InlineCompletionService } from '../service/inlineCompletionService' import { ClassifierTrigger } from './classifierTrigger' import { DefaultCodeWhispererClient } from '../client/codewhisperer' @@ -81,67 +77,7 @@ export class RecommendationService { const traceId = telemetry.attributes?.traceId ?? randomUUID() TelemetryHelper.instance.setTraceId(traceId) await telemetry.withTraceId(async () => { - if (isCloud9('any')) { - // C9 manual trigger key alt/option + C is ALWAYS enabled because the VSC version C9 is on doesn't support setContextKey which is used for CODEWHISPERER_ENABLED - // therefore we need a connection check if there is ANY connection(regardless of the connection's state) connected to CodeWhisperer on C9 - if (triggerType === 'OnDemand' && !AuthUtil.instance.isConnected()) { - return - } - - RecommendationHandler.instance.checkAndResetCancellationTokens() - vsCodeState.isIntelliSenseActive = false - this._isRunning = true - let response: GetRecommendationsResponse = { - result: 'Failed', - errorMessage: undefined, - recommendationCount: 0, - } - - try { - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: true, - triggerType: triggerType, - response: undefined, - }) - - if (isCloud9('classic') || isIamConnection(AuthUtil.instance.conn)) { - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - false - ) - } else { - if (AuthUtil.instance.isConnectionExpired()) { - await AuthUtil.instance.showReauthenticatePrompt() - } - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - true - ) - } - if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, true, response)) { - await vscode.commands.executeCommand('editor.action.triggerSuggest').then(() => { - vsCodeState.isIntelliSenseActive = true - }) - } - } finally { - this._isRunning = false - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: false, - triggerType: triggerType, - response: response, - }) - } - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { if (triggerType === 'OnDemand') { ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) } diff --git a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 2a2fe867321..02055f0aded 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -9,9 +9,8 @@ import { LicenseUtil } from '../util/licenseUtil' import * as CodeWhispererConstants from '../models/constants' import { CodeWhispererSettings } from '../util/codewhispererSettings' import globals from '../../shared/extensionGlobals' -import { isCloud9 } from '../../shared/extensionUtilities' import { AuthUtil } from '../util/authUtil' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.codeWhisperer.referenceLog' @@ -69,6 +68,7 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { reference.recommendationContentSpan.start, reference.recommendationContentSpan.end ) + const session = CodeWhispererSessionState.instance.getSession() const firstCharLineNumber = editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.start).line + 1 @@ -127,22 +127,9 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { } } - let csp = '' - if (isCloud9()) { - csp = `` - } return ` - ${csp} diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index e76a201be87..47490f2427f 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -4,7 +4,14 @@ */ import * as vscode from 'vscode' import path from 'path' -import { CodeScanIssue, SecurityTreeViewFilterState, severities, Severity } from '../models/model' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + CodeScanIssue, + SecurityTreeViewFilterState, + severities, + Severity, +} from '../models/model' import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger' import { SecurityIssueProvider } from './securityIssueProvider' @@ -34,6 +41,17 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + switch (groupingStrategy) { + case CodeIssueGroupingStrategy.FileLocation: + return this.getChildrenGroupedByFileLocation(element) + case CodeIssueGroupingStrategy.Severity: + default: + return this.getChildrenGroupedBySeverity(element) + } + } + + private getChildrenGroupedBySeverity(element: SecurityViewTreeItem | undefined) { const filterHiddenSeverities = (severity: Severity) => !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(severity) @@ -64,6 +82,27 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider + !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(issue.severity) + + if (element instanceof FileItem) { + return element.issues + .filter(filterHiddenSeverities) + .filter((issue) => issue.visible) + .sort((a, b) => a.startLine - b.startLine) + .map((issue) => new IssueItem(element.filePath, issue)) + } + + const result = this.issueProvider.issues + .filter((group) => group.issues.some(filterHiddenSeverities)) + .filter((group) => group.issues.some((issue) => issue.visible)) + .sort((a, b) => a.filePath.localeCompare(b.filePath)) + .map((group) => new FileItem(group.filePath, group.issues.filter(filterHiddenSeverities))) + this._onDidChangeTreeData.fire(result) + return result + } + public refresh(): void { this._onDidChangeTreeData.fire() } @@ -118,7 +157,8 @@ export class IssueItem extends vscode.TreeItem { public readonly issue: CodeScanIssue ) { super(issue.title, vscode.TreeItemCollapsibleState.None) - this.description = `${path.basename(this.filePath)} [Ln ${this.issue.startLine + 1}, Col 1]` + this.description = this.getDescription() + this.iconPath = this.getSeverityIcon() this.tooltip = this.getTooltipMarkdown() this.command = { title: 'Focus Issue', @@ -132,6 +172,22 @@ export class IssueItem extends vscode.TreeItem { return globals.context.asAbsolutePath(`resources/images/severity-${this.issue.severity.toLowerCase()}.svg`) } + private getSeverityIcon() { + const iconPath = globals.context.asAbsolutePath( + `resources/icons/aws/amazonq/severity-${this.issue.severity.toLowerCase()}.svg` + ) + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.Severity ? iconPath : undefined + } + + private getDescription() { + const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation + ? `${path.basename(this.filePath)} ${positionStr}` + : positionStr + } + private getContextValue() { return this.issue.suggestedFixes.length === 0 || !this.issue.suggestedFixes[0].code ? ContextValue.ISSUE_WITHOUT_FIX diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index ba70e558733..a8db65603b3 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -43,6 +43,8 @@ import { getTelemetryReasonDesc } from '../../shared/errors' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { detectCommentAboveLine } from '../../shared/utilities/commentUtils' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { FeatureUseCase } from '../models/constants' +import { UploadTestArtifactToS3Error } from '../../amazonqTest/error' export async function listScanResults( client: DefaultCodeWhispererClient, @@ -81,7 +83,7 @@ export async function listScanResults( if (existsSync(filePath) && statSync(filePath).isFile()) { const aggregatedCodeScanIssue: AggregatedCodeScanIssue = { filePath: filePath, - issues: issues.map((issue) => mapRawToCodeScanIssue(issue, editor, jobId)), + issues: issues.map((issue) => mapRawToCodeScanIssue(issue, editor, jobId, scope)), } aggregatedCodeScanIssueList.push(aggregatedCodeScanIssue) } @@ -90,7 +92,7 @@ export async function listScanResults( if (existsSync(maybeAbsolutePath) && statSync(maybeAbsolutePath).isFile()) { const aggregatedCodeScanIssue: AggregatedCodeScanIssue = { filePath: maybeAbsolutePath, - issues: issues.map((issue) => mapRawToCodeScanIssue(issue, editor, jobId)), + issues: issues.map((issue) => mapRawToCodeScanIssue(issue, editor, jobId, scope)), } aggregatedCodeScanIssueList.push(aggregatedCodeScanIssue) } @@ -101,7 +103,8 @@ export async function listScanResults( function mapRawToCodeScanIssue( issue: RawCodeScanIssue, editor: vscode.TextEditor | undefined, - jobId: string + jobId: string, + scope: CodeWhispererConstants.CodeAnalysisScope ): CodeScanIssue { const isIssueTitleIgnored = CodeWhispererSettings.instance.getIgnoredSecurityIssues().includes(issue.title) const isSingleIssueIgnored = @@ -128,6 +131,7 @@ function mapRawToCodeScanIssue( visible: !isIssueTitleIgnored && !isSingleIssueIgnored, scanJobId: jobId, language, + autoDetected: scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO, } } @@ -287,7 +291,7 @@ export async function getPresignedUrlAndUpload( logger.verbose(`CreateUploadUrlRequest request id: ${srcResp.$response.requestId}`) logger.verbose(`Complete Getting presigned Url for uploading src context.`) logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, scope) + await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, FeatureUseCase.CODE_SCAN, scope) logger.verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -343,6 +347,7 @@ export function throwIfCancelled(scope: CodeWhispererConstants.CodeAnalysisScope export async function uploadArtifactToS3( fileName: string, resp: CreateUploadUrlResponse, + featureUseCase: FeatureUseCase, scope?: CodeWhispererConstants.CodeAnalysisScope ) { const logger = getLoggerForScope(scope) @@ -365,14 +370,23 @@ export async function uploadArtifactToS3( }).response logger.debug(`StatusCode: ${response.status}, Text: ${response.statusText}`) } catch (error) { + let errorMessage = '' + const isCodeScan = featureUseCase === FeatureUseCase.CODE_SCAN + const featureType = isCodeScan ? 'security scans' : 'unit test generation' + const defaultMessage = isCodeScan ? 'Security scan failed.' : 'Test generation failed.' getLogger().error( - `Amazon Q is unable to upload workspace artifacts to Amazon S3 for security scans. For more information, see the Amazon Q documentation or contact your network or organization administrator.` + `Amazon Q is unable to upload workspace artifacts to Amazon S3 for ${featureType}. ` + + 'For more information, see the Amazon Q documentation or contact your network or organization administrator.' ) - const errorMessage = getTelemetryReasonDesc(error)?.includes(`"PUT" request failed with code "403"`) - ? `"PUT" request failed with code "403"` - : (getTelemetryReasonDesc(error) ?? 'Security scan failed.') - - throw new UploadArtifactToS3Error(errorMessage) + const errorDesc = getTelemetryReasonDesc(error) + if (errorDesc?.includes('"PUT" request failed with code "403"')) { + errorMessage = '"PUT" request failed with code "403"' + } else if (errorDesc?.includes('"PUT" request failed with code "503"')) { + errorMessage = '"PUT" request failed with code "503"' + } else { + errorMessage = errorDesc ?? defaultMessage + } + throw isCodeScan ? new UploadArtifactToS3Error(errorMessage) : new UploadTestArtifactToS3Error(errorMessage) } } @@ -391,10 +405,7 @@ function getPollingDelayMsForScope(scope: CodeWhispererConstants.CodeAnalysisSco } function getPollingTimeoutMsForScope(scope: CodeWhispererConstants.CodeAnalysisScope) { - return ( - (scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO || - scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND - ? CodeWhispererConstants.codeFileScanJobTimeoutSeconds - : CodeWhispererConstants.codeScanJobTimeoutSeconds) * 1000 - ) + return scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO + ? CodeWhispererConstants.expressScanTimeoutMs + : CodeWhispererConstants.standardScanTimeoutMs } diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts index 203dcd8eb03..48a66fb1f83 100644 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ b/packages/core/src/codewhisperer/service/testGenHandler.ts @@ -13,10 +13,18 @@ import CodeWhispererUserClient, { CreateUploadUrlRequest, TargetCode, } from '../client/codewhispereruserclient' -import { CreateUploadUrlError, InvalidSourceZipError, TestGenFailedError, TestGenTimedOutError } from '../models/errors' +import { + CreateTestJobError, + CreateUploadUrlError, + ExportResultsArchiveError, + InvalidSourceZipError, + TestGenFailedError, + TestGenStoppedError, + TestGenTimedOutError, +} from '../../amazonqTest/error' import { getMd5, uploadArtifactToS3 } from './securityScanHandler' import { fs, randomUUID, sleep, tempDirPath } from '../../shared' -import { ShortAnswer, TestGenerationJobStatus, testGenState } from '..' +import { ShortAnswer, testGenState } from '../models/model' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../shared/utilities/download' @@ -24,12 +32,13 @@ import AdmZip from 'adm-zip' import path from 'path' import { ExportIntent } from '@amzn/codewhisperer-streaming' import { glob } from 'glob' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' // TODO: Get TestFileName and Framework and to error message export function throwIfCancelled() { // TODO: fileName will be '' if user gives propt without opening if (testGenState.isCancelling()) { - throw Error(CodeWhispererConstants.unitTestGenerationCancelMessage) + throw new TestGenStoppedError() } } @@ -47,12 +56,12 @@ export async function getPresignedUrlAndUploadTestGen(zipMetadata: ZipMetadata) logger.verbose(`Prepare for uploading src context...`) const srcResp = await codeWhisperer.codeWhispererClient.createUploadUrl(srcReq).catch((err) => { getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err) + throw new CreateUploadUrlError(err.message) }) logger.verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) logger.verbose(`Complete Getting presigned Url for uploading src context.`) logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp) + await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.TEST_GENERATION) logger.verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -96,7 +105,7 @@ export async function createTestJob( const resp = await codewhispererClient.codeWhispererClient.startTestGeneration(req).catch((err) => { ChatSessionManager.Instance.getSession().startTestGenerationRequestId = err.requestId logger.error(`Failed creating test job. Request id: ${err.requestId}`) - throw err + throw new CreateTestJobError(err.message) }) logger.info('Unit test generation request id: %s', resp.$response.requestId) logger.debug('Unit test generation data: %O', resp.$response.data) @@ -182,9 +191,9 @@ export async function pollTestJobStatus( } ChatSessionManager.Instance.getSession().shortAnswer = shortAnswer } - if (resp.testGenerationJob?.status !== TestGenerationJobStatus.IN_PROGRESS) { + if (resp.testGenerationJob?.status !== CodeWhispererConstants.TestGenerationJobStatus.IN_PROGRESS) { // This can be FAILED or COMPLETED - status = resp.testGenerationJob?.status as TestGenerationJobStatus + status = resp.testGenerationJob?.status as CodeWhispererConstants.TestGenerationJobStatus logger.verbose(`testgen job status: ${status}`) logger.verbose(`Complete polling test job status.`) break @@ -231,7 +240,6 @@ export async function exportResultsArchive( const zip = new AdmZip(pathToArchive) zip.extractAllTo(pathToArchiveDir, true) - const session = ChatSessionManager.Instance.getSession() const testFilePathFromResponse = session?.shortAnswer?.testFilePath const testFilePath = testFilePathFromResponse ? testFilePathFromResponse.split('/').slice(1).join('/') // remove the project name @@ -253,7 +261,7 @@ export async function exportResultsArchive( session.numberOfTestsGenerated = 0 downloadErrorMessage = (e as Error).message getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new Error('Error downloading test generation result artifacts: ' + downloadErrorMessage) + throw new ExportResultsArchiveError(downloadErrorMessage) } } @@ -292,8 +300,9 @@ export async function downloadResultArchive( } catch (e: any) { downloadErrorMessage = (e as Error).message getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw e + throw new ExportResultsArchiveError(downloadErrorMessage) } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index cbf6bf92710..b5f4d2d1447 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -49,6 +49,7 @@ import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSess import { encodeHTML } from '../../../shared/utilities/textUtilities' import { convertToTimeString } from '../../../shared/datetime' import { getAuthType } from '../../../auth/utils' +import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker' export function getSha256(buffer: Buffer) { const hasher = crypto.createHash('sha256') @@ -447,6 +448,7 @@ export async function startJob(uploadId: string) { target: { language: targetLanguageVersion }, // always JDK17 }, }) + getLogger().info('CodeTransformation: called startJob API successfully') if (response.$response.requestId) { transformByQState.setJobFailureMetadata(` (request ID: ${response.$response.requestId})`) } @@ -670,6 +672,7 @@ export async function pollTransformationJob(jobId: string, validStates: string[] }) } transformByQState.setPolledJobStatus(status) + getLogger().info(`CodeTransformation: polled job status = ${status}`) const errorMessage = response.transformationJob.reason if (errorMessage !== undefined) { @@ -767,6 +770,7 @@ export async function downloadResultArchive( throw e } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts index 8ba8504e436..e2bbbc6556b 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts @@ -11,7 +11,7 @@ import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-i import { CodeTransformBuildCommand, telemetry } from '../../../shared/telemetry/telemetry' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { ToolkitError } from '../../../shared/errors' -import { writeLogs } from './transformFileHandler' +import { setMaven, writeLogs } from './transformFileHandler' import { throwIfCancelled } from './transformApiHandler' // run 'install' with either 'mvnw.cmd', './mvnw', or 'mvn' (if wrapper exists, we use that, otherwise we use regular 'mvn') @@ -108,6 +108,8 @@ function copyProjectDependencies(dependenciesFolder: FolderInfo, modulePath: str } export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, rootPomPath: string) { + await setMaven() + getLogger().info('CodeTransformation: running Maven copy-dependencies') try { copyProjectDependencies(dependenciesFolder, rootPomPath) } catch (err) { @@ -117,6 +119,7 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, ) } + getLogger().info('CodeTransformation: running Maven install') try { installProjectDependencies(dependenciesFolder, rootPomPath) } catch (err) { @@ -134,9 +137,9 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, export async function getVersionData() { const baseCommand = transformByQState.getMavenName() // will be one of: 'mvnw.cmd', './mvnw', 'mvn' - const modulePath = transformByQState.getProjectPath() + const projectPath = transformByQState.getProjectPath() const args = ['-v'] - const spawnResult = spawnSync(baseCommand, args, { cwd: modulePath, shell: true, encoding: 'utf-8' }) + const spawnResult = spawnSync(baseCommand, args, { cwd: projectPath, shell: true, encoding: 'utf-8' }) let localMavenVersion: string | undefined = '' let localJavaVersion: string | undefined = '' diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 98bdae3fcf4..ef2ce722598 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -98,7 +98,7 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider -

Job Status

+

Transformation Status

${ Object.keys(sessionJobHistory).length === 0 ? `

${CodeWhispererConstants.nothingToShowMessage}

` diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index 43c6a1cc08b..9339be10fc9 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -26,6 +26,7 @@ import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/ import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { setContext } from '../../../shared/vscode/setContext' import * as codeWhisperer from '../../client/codewhisperer' +import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker' export abstract class ProposedChangeNode { abstract readonly resourcePath: string @@ -177,6 +178,7 @@ export class DiffModel { } const changedFiles = parsePatch(diffContents) + getLogger().info('CodeTransformation: parsed patch file successfully') // path to the directory containing copy of the changed files in the transformed project const pathToTmpSrcDir = this.copyProject(pathToWorkspace, changedFiles) transformByQState.setProjectCopyFilePath(pathToTmpSrcDir) @@ -401,6 +403,7 @@ export class ProposedTransformationExplorer { pathToArchive ) + getLogger().info('CodeTransformation: downloaded results successfully') // Update downloaded artifact size exportResultsArchiveSize = (await fs.promises.stat(pathToArchive)).size @@ -424,6 +427,7 @@ export class ProposedTransformationExplorer { throw new Error('Error downloading diff') } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } let deserializeErrorMessage = undefined @@ -532,6 +536,7 @@ export class ProposedTransformationExplorer { vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.acceptChanges', async () => { telemetry.codeTransform_submitSelection.run(() => { + getLogger().info('CodeTransformation: accepted changes') diffModel.saveChanges() telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), @@ -585,6 +590,7 @@ export class ProposedTransformationExplorer { vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.rejectChanges', async () => { await telemetry.codeTransform_submitSelection.run(async () => { + getLogger().info('CodeTransformation: rejected changes') diffModel.rejectChanges() await reset() telemetry.record({ diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts index d0ad76c26da..39416eafe70 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -27,6 +27,8 @@ const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] /** * This singleton class is mainly used for calculating the code written by codeWhisperer + * TODO: Remove this tracker, uses user written code tracker instead. + * This is kept in codebase for server side backward compatibility until service fully switch to user written code */ export class CodeWhispererCodeCoverageTracker { private _acceptedTokens: { [key: string]: CodeWhispererToken[] } diff --git a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts new file mode 100644 index 00000000000..2497006b0a4 --- /dev/null +++ b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codeWhispererClient as client } from '../client/codewhisperer' +import { isAwsError } from '../../shared/errors' +import { CodewhispererLanguage, globals, undefinedIfEmpty } from '../../shared' + +/** + * This singleton class is mainly used for calculating the user written code + * for active Amazon Q users. + * It reports the user written code per 5 minutes when the user is coding and using Amazon Q features + */ +export class UserWrittenCodeTracker { + private _userWrittenNewCodeCharacterCount: Map + private _userWrittenNewCodeLineCount: Map + private _qIsMakingEdits: boolean + private _timer?: NodeJS.Timer + private _qUsageCount: number + private _lastQInvocationTime: number + + static #instance: UserWrittenCodeTracker + private static copySnippetThreshold = 50 + private static resetQIsEditingTimeoutMs = 2 * 60 * 1000 + private static defaultCheckPeriodMillis = 5 * 60 * 1000 + + private constructor() { + this._userWrittenNewCodeLineCount = new Map() + this._userWrittenNewCodeCharacterCount = new Map() + this._qUsageCount = 0 + this._qIsMakingEdits = false + this._timer = undefined + this._lastQInvocationTime = 0 + } + + public static get instance() { + return (this.#instance ??= new this()) + } + + public isActive(): boolean { + return globals.telemetry.telemetryEnabled && AuthUtil.instance.isConnected() + } + + // this should be invoked whenever there is a successful Q feature invocation + // for all Q features + public onQFeatureInvoked() { + this._qUsageCount += 1 + this._lastQInvocationTime = performance.now() + } + + public onQStartsMakingEdits() { + this._qIsMakingEdits = true + } + + public onQFinishesEdits() { + this._qIsMakingEdits = false + } + + public getUserWrittenCharacters(language: CodewhispererLanguage) { + return this._userWrittenNewCodeCharacterCount.get(language) || 0 + } + + public getUserWrittenLines(language: CodewhispererLanguage) { + return this._userWrittenNewCodeLineCount.get(language) || 0 + } + + public reset() { + this._userWrittenNewCodeLineCount = new Map() + this._userWrittenNewCodeCharacterCount = new Map() + this._qUsageCount = 0 + this._qIsMakingEdits = false + this._lastQInvocationTime = 0 + if (this._timer !== undefined) { + clearTimeout(this._timer) + this._timer = undefined + } + } + + public emitCodeContributions() { + const selectedCustomization = getSelectedCustomization() + + for (const [language, charCount] of this._userWrittenNewCodeCharacterCount) { + const lineCount = this.getUserWrittenLines(language) + if (charCount > 0) { + client + .sendTelemetryEvent({ + telemetryEvent: { + codeCoverageEvent: { + customizationArn: undefinedIfEmpty(selectedCustomization.arn), + programmingLanguage: { + languageName: runtimeLanguageContext.toRuntimeLanguage(language), + }, + acceptedCharacterCount: 0, + totalCharacterCount: 0, + timestamp: new Date(Date.now()), + userWrittenCodeCharacterCount: charCount, + userWrittenCodeLineCount: lineCount, + }, + }, + }) + .then() + .catch((error) => { + let requestId: string | undefined + if (isAwsError(error)) { + requestId = error.requestId + } + getLogger().debug( + `Failed to sendTelemetryEvent, requestId: ${requestId ?? ''}, message: ${error.message}` + ) + }) + } + } + } + + private tryStartTimer() { + if (this._timer !== undefined) { + return + } + if (!this.isActive()) { + getLogger().debug(`Skip emiting code contribution metric. Telemetry disabled or not logged in. `) + this.reset() + return + } + const startTime = performance.now() + this._timer = setTimeout(() => { + try { + const currentTime = performance.now() + const delay: number = UserWrittenCodeTracker.defaultCheckPeriodMillis + const diffTime: number = startTime + delay + if (diffTime <= currentTime) { + if (this._qUsageCount <= 0) { + getLogger().debug(`Skip emiting code contribution metric. There is no active Amazon Q usage. `) + return + } + if (this._userWrittenNewCodeCharacterCount.size === 0) { + getLogger().debug(`Skip emiting code contribution metric. There is no new code added. `) + return + } + this.emitCodeContributions() + } + } catch (e) { + getLogger().verbose(`Exception Thrown from QCodeGenTracker: ${e}`) + } finally { + this.reset() + } + }, UserWrittenCodeTracker.defaultCheckPeriodMillis) + } + + private countNewLines(str: string) { + return str.split('\n').length - 1 + } + + public onTextDocumentChange(e: vscode.TextDocumentChangeEvent) { + // do not count code written by Q as user written code + if ( + !runtimeLanguageContext.isLanguageSupported(e.document.languageId) || + e.contentChanges.length === 0 || + this._qIsMakingEdits + ) { + // if the boolean of qIsMakingEdits was incorrectly set to true + // due to unhandled edge cases or early terminated code paths + // reset it back to false after a reasonable period of time + if (this._qIsMakingEdits) { + if (performance.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { + getLogger().warn(`Reset Q is editing state to false.`) + this._qIsMakingEdits = false + } + } + return + } + const contentChange = e.contentChanges[0] + // if user copies code into the editor for more than 50 characters + // do not count this as total new code, this will skew the data, + // reporting highly inflated user written code + if (contentChange.text.length > UserWrittenCodeTracker.copySnippetThreshold) { + return + } + const language = runtimeLanguageContext.normalizeLanguage(e.document.languageId) + if (language) { + const charCount = this.getUserWrittenCharacters(language) + this._userWrittenNewCodeCharacterCount.set(language, charCount + contentChange.text.length) + const lineCount = this.getUserWrittenLines(language) + this._userWrittenNewCodeLineCount.set(language, lineCount + this.countNewLines(contentChange.text)) + // start 5 min data reporting once valid user input is detected + this.tryStartTimer() + } + } +} diff --git a/packages/core/src/codewhisperer/ui/prompters.ts b/packages/core/src/codewhisperer/ui/prompters.ts new file mode 100644 index 00000000000..95541d84a82 --- /dev/null +++ b/packages/core/src/codewhisperer/ui/prompters.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + codeIssueGroupingStrategies, + CodeIssueGroupingStrategy, + codeIssueGroupingStrategyLabel, + CodeIssueGroupingStrategyState, +} from '../models/model' +import { createQuickPick, QuickPickPrompter } from '../../shared/ui/pickerPrompter' +import { localize } from '../../shared/utilities/vsCodeUtils' + +export function createCodeIssueGroupingStrategyPrompter(): QuickPickPrompter { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + const prompter = createQuickPick( + codeIssueGroupingStrategies.map((strategy) => ({ + data: strategy, + label: codeIssueGroupingStrategyLabel[strategy], + })), + { + title: localize('AWS.amazonq.scans.groupIssues', 'Group Issues'), + placeholder: localize('AWS.amazonq.scans.groupIssues.placeholder', 'Select how to group code issues'), + } + ) + prompter.quickPick.activeItems = prompter.quickPick.items.filter((item) => item.data === groupingStrategy) + prompter.quickPick.onDidChangeSelection(async (items) => { + const [item] = items + await CodeIssueGroupingStrategyState.instance.setState(item.data) + prompter.quickPick.hide() + }) + return prompter +} diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 461f262d34d..a9045220ea2 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -8,7 +8,7 @@ import * as localizedText from '../../shared/localizedText' import { Auth } from '../../auth/auth' import { ToolkitError, isNetworkError, tryRun } from '../../shared/errors' import { getSecondaryAuth, setScopes } from '../../auth/secondaryAuth' -import { isCloud9, isSageMaker } from '../../shared/extensionUtilities' +import { isSageMaker } from '../../shared/extensionUtilities' import { AmazonQPromptSettings } from '../../shared/settings' import { scopesCodeWhispererCore, @@ -55,14 +55,8 @@ export const amazonQScopes = [...codeWhispererChatScopes, ...scopesGumby, ...sco * for Amazon Q. */ export const isValidCodeWhispererCoreConnection = (conn?: Connection): conn is Connection => { - if (isCloud9('classic')) { - return isIamConnection(conn) - } - return ( - (isSageMaker() && isIamConnection(conn)) || - (isCloud9('codecatalyst') && isIamConnection(conn)) || - (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) + (isSageMaker() && isIamConnection(conn)) || (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) ) } /** Superset that includes all of CodeWhisperer + Amazon Q */ @@ -144,10 +138,6 @@ export class AuthUtil { }) public async setVscodeContextProps() { - if (isCloud9()) { - return - } - await setContext('aws.codewhisperer.connected', this.isConnected()) const doShowAmazonQLoginView = !this.isConnected() || this.isConnectionExpired() await setContext('aws.amazonq.showLoginView', doShowAmazonQLoginView) diff --git a/packages/core/src/codewhisperer/util/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts index b9511349e9c..466ca31a0b9 100644 --- a/packages/core/src/codewhisperer/util/closingBracketUtil.ts +++ b/packages/core/src/codewhisperer/util/closingBracketUtil.ts @@ -4,8 +4,6 @@ */ import * as vscode from 'vscode' -import { workspace, WorkspaceEdit } from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import * as CodeWhispererConstants from '../models/constants' interface bracketMapType { @@ -97,31 +95,18 @@ const removeBracketsFromRightContext = async ( ) => { const offset = editor.document.offsetAt(endPosition) - if (isCloud9()) { - const edits = idxToRemove.map((idx) => ({ - range: new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ), - newText: '', - })) - const wEdit = new WorkspaceEdit() - wEdit.set(editor.document.uri, [...edits]) - await workspace.applyEdit(wEdit) - } else { - await editor.edit( - (editBuilder) => { - for (const idx of idxToRemove) { - const range = new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ) - editBuilder.delete(range) - } - }, - { undoStopAfter: false, undoStopBefore: false } - ) - } + await editor.edit( + (editBuilder) => { + for (const idx of idxToRemove) { + const range = new vscode.Range( + editor.document.positionAt(offset + idx), + editor.document.positionAt(offset + idx + 1) + ) + editBuilder.delete(range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) } function getBracketsToRemove( diff --git a/packages/core/src/codewhisperer/util/codeWhispererSession.ts b/packages/core/src/codewhisperer/util/codeWhispererSession.ts index 042cd947124..12222903568 100644 --- a/packages/core/src/codewhisperer/util/codeWhispererSession.ts +++ b/packages/core/src/codewhisperer/util/codeWhispererSession.ts @@ -14,41 +14,62 @@ import { GenerateRecommendationsRequest, ListRecommendationsRequest, Recommendat import { Position } from 'vscode' import { CodeWhispererSupplementalContext, vsCodeState } from '../models/model' -class CodeWhispererSession { - static #instance: CodeWhispererSession +export class CodeWhispererSessionState { + static #instance: CodeWhispererSessionState + session: CodeWhispererSession + nextSession: CodeWhispererSession + + constructor() { + this.session = new CodeWhispererSession() + this.nextSession = new CodeWhispererSession() + } + public static get instance() { + return (this.#instance ??= new CodeWhispererSessionState()) + } + + getSession() { + return this.session + } + + setSession(session: CodeWhispererSession) { + this.session = session + } + + getNextSession() { + return this.nextSession + } - // Per-session states - sessionId = '' + setNextSession(session: CodeWhispererSession) { + this.nextSession = session + } +} + +export class CodeWhispererSession { + sessionId: string = '' requestIdList: string[] = [] - startPos = new Position(0, 0) - startCursorOffset = 0 - leftContextOfCurrentLine = '' + startPos: Position = new Position(0, 0) + startCursorOffset: number = 0 + leftContextOfCurrentLine: string = '' requestContext: { request: ListRecommendationsRequest | GenerateRecommendationsRequest supplementalMetadata: CodeWhispererSupplementalContext | undefined - } = { request: {} as any, supplementalMetadata: {} as any } + } = { request: {} as any, supplementalMetadata: undefined } language: CodewhispererLanguage = 'python' - taskType: CodewhispererGettingStartedTask | undefined + taskType: CodewhispererGettingStartedTask | undefined = undefined triggerType: CodewhispererTriggerType = 'OnDemand' - autoTriggerType: CodewhispererAutomatedTriggerType | undefined - + autoTriggerType: CodewhispererAutomatedTriggerType | undefined = undefined // Various states of recommendations recommendations: Recommendation[] = [] - suggestionStates = new Map() - completionTypes = new Map() - + suggestionStates: Map = new Map() + completionTypes: Map = new Map() // Some other variables for client component latency - fetchCredentialStartTime = 0 - sdkApiCallStartTime = 0 - invokeSuggestionStartTime = 0 - preprocessEndTime = 0 - timeToFirstRecommendation = 0 - firstSuggestionShowTime = 0 - perceivedLatency = 0 - - public static get instance() { - return (this.#instance ??= new CodeWhispererSession()) - } + fetchCredentialStartTime: number = 0 + sdkApiCallStartTime: number = 0 + invokeSuggestionStartTime: number = 0 + preprocessEndTime: number = 0 + timeToFirstRecommendation: number = 0 + firstSuggestionShowTime: number = 0 + perceivedLatency: number = 0 setFetchCredentialStart() { if (this.fetchCredentialStartTime === 0 && this.invokeSuggestionStartTime !== 0) { @@ -89,7 +110,7 @@ class CodeWhispererSession { if (triggerType === 'OnDemand') { return this.timeToFirstRecommendation } else { - return session.firstSuggestionShowTime - vsCodeState.lastUserModificationTime + return this.firstSuggestionShowTime - vsCodeState.lastUserModificationTime } } @@ -118,6 +139,3 @@ class CodeWhispererSession { this.completionTypes.clear() } } - -// TODO: convert this to a function call -export const session = CodeWhispererSession.instance diff --git a/packages/core/src/codewhisperer/util/codewhispererSettings.ts b/packages/core/src/codewhisperer/util/codewhispererSettings.ts index 2bf2f657867..8a52627aacb 100644 --- a/packages/core/src/codewhisperer/util/codewhispererSettings.ts +++ b/packages/core/src/codewhisperer/util/codewhispererSettings.ts @@ -13,6 +13,7 @@ const description = { workspaceIndexWorkerThreads: Number, workspaceIndexUseGPU: Boolean, workspaceIndexMaxSize: Number, + allowFeatureDevelopmentToRunCodeAndTests: Object, ignoredSecurityIssues: ArrayConstructor(String), } @@ -50,6 +51,18 @@ export class CodeWhispererSettings extends fromExtensionManifest('amazonQ', desc return Math.max(this.get('workspaceIndexMaxSize', 250), 1) } + public getAutoBuildSetting(): { [key: string]: boolean } { + return this.get('allowFeatureDevelopmentToRunCodeAndTests', {}) + } + + public async updateAutoBuildSetting(projectName: string, setting: boolean) { + const projects = this.getAutoBuildSetting() + + projects[projectName] = setting + + await this.update('allowFeatureDevelopmentToRunCodeAndTests', projects) + } + public getIgnoredSecurityIssues(): string[] { return this.get('ignoredSecurityIssues', []) } diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index 1d624e77b5e..d2df78f1369 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode' import * as semver from 'semver' import { distance } from 'fastest-levenshtein' -import { isCloud9 } from '../../shared/extensionUtilities' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' import { AWSTemplateCaseInsensitiveKeyWords, @@ -31,12 +30,12 @@ export function asyncCallWithTimeout(asyncPromise: Promise, message: strin } export function isInlineCompletionEnabled() { - return getInlineSuggestEnabled() && !isCloud9() + return getInlineSuggestEnabled() } // This is the VS Code version that started to have regressions in inline completion API export function isVscHavingRegressionInlineCompletionApi() { - return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() && !isCloud9() + return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() } export function getFileExt(languageId: string) { diff --git a/packages/core/src/codewhisperer/util/customizationUtil.ts b/packages/core/src/codewhisperer/util/customizationUtil.ts index e87d17cfdb3..0ede7c983f9 100644 --- a/packages/core/src/codewhisperer/util/customizationUtil.ts +++ b/packages/core/src/codewhisperer/util/customizationUtil.ts @@ -113,7 +113,7 @@ export const getSelectedCustomization = (): Customization => { ) const selectedCustomization = selectedCustomizationArr[AuthUtil.instance.conn.label] - if (selectedCustomization && selectedCustomization.name !== baseCustomization.name) { + if (selectedCustomization && selectedCustomization.name !== '') { return selectedCustomization } else { const customizationFeature = FeatureConfigProvider.getFeature(Features.customizationArnOverride) diff --git a/packages/core/src/codewhisperer/util/showSsoPrompt.ts b/packages/core/src/codewhisperer/util/showSsoPrompt.ts index fbe1e6cb41c..229583a8cc2 100644 --- a/packages/core/src/codewhisperer/util/showSsoPrompt.ts +++ b/packages/core/src/codewhisperer/util/showSsoPrompt.ts @@ -14,15 +14,12 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils' import { ToolkitError } from '../../shared/errors' import { createCommonButtons } from '../../shared/ui/buttons' import { telemetry } from '../../shared/telemetry/telemetry' -import { isCloud9 } from '../../shared/extensionUtilities' import { createBuilderIdItem, createSsoItem, createIamItem } from '../../auth/utils' import { Commands } from '../../shared/vscode/commands2' import { vsCodeState } from '../models/model' export const showCodeWhispererConnectionPrompt = async () => { - const items = isCloud9('classic') - ? [createSsoItem(), createCodeWhispererIamItem()] - : [createBuilderIdItem(), createSsoItem(), createCodeWhispererIamItem()] + const items = [createBuilderIdItem(), createSsoItem(), createCodeWhispererIamItem()] const resp = await showQuickPick(items, { title: 'Amazon Q: Add Connection to AWS', diff --git a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts index a4b8aa5fba1..f4688e2b5a9 100644 --- a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts +++ b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts @@ -7,7 +7,11 @@ import * as vscode from 'vscode' import { FeatureConfigProvider, fs } from '../../../shared' import path = require('path') import { BM25Document, BM25Okapi } from './rankBm25' -import { crossFileContextConfig, supplementalContextTimeoutInMs } from '../../models/constants' +import { + crossFileContextConfig, + supplementalContextTimeoutInMs, + supplementalContextMaxTotalLength, +} from '../../models/constants' import { isTestFile } from './codeParsingUtil' import { getFileDistance } from '../../../shared/filesystemUtilities' import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' @@ -84,13 +88,13 @@ export async function fetchSupplementalContextForSrc( const supContext = (await opentabsContextPromise) ?? [] return { supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'Empty' : 'opentabs', + strategy: supContext.length === 0 ? 'empty' : 'opentabs', } } // codemap will use opentabs context plus repomap if it's present if (supplementalContextConfig === 'codemap') { - let strategy: SupplementalContextStrategy = 'Empty' + let strategy: SupplementalContextStrategy = 'empty' let hasCodemap: boolean = false let hasOpentabs: boolean = false const opentabsContextAndCodemap = await waitUntil( @@ -99,13 +103,22 @@ export async function fetchSupplementalContextForSrc( const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) const codemap = await fetchProjectContext(editor, 'codemap') + function addToResult(items: CodeWhispererSupplementalContextItem[]) { + for (const item of items) { + const curLen = result.reduce((acc, i) => acc + i.content.length, 0) + if (curLen + item.content.length < supplementalContextMaxTotalLength) { + result.push(item) + } + } + } + if (codemap && codemap.length > 0) { - result.push(...codemap) + addToResult(codemap) hasCodemap = true } if (opentabsContext && opentabsContext.length > 0) { - result.push(...opentabsContext) + addToResult(opentabsContext) hasOpentabs = true } @@ -119,7 +132,7 @@ export async function fetchSupplementalContextForSrc( } else if (hasOpentabs) { strategy = 'opentabs' } else { - strategy = 'Empty' + strategy = 'empty' } return { @@ -148,7 +161,7 @@ export async function fetchSupplementalContextForSrc( const supContext = opentabsContext ?? [] return { supplementalContextItems: supContext, - strategy: supContext.length === 0 ? 'Empty' : 'opentabs', + strategy: supContext.length === 0 ? 'empty' : 'opentabs', } } diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts index 5cb7c2bfb83..03f9d59b3f2 100644 --- a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts +++ b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts @@ -58,7 +58,7 @@ export async function fetchSupplementalContext( supplementalContextItems: [], contentsLength: 0, latency: performance.now() - timesBeforeFetching, - strategy: 'Empty', + strategy: 'empty', } } else { getLogger().error( diff --git a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts index 63c29dc1c9a..a39a48183b0 100644 --- a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts +++ b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts @@ -54,7 +54,7 @@ export async function fetchSupplementalContextForTest( const shouldProceed = shouldFetchUtgContext(editor.document.languageId) if (!shouldProceed) { - return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'Empty' } + return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'empty' } } const languageConfig = utgLanguageConfigs[editor.document.languageId] @@ -69,10 +69,10 @@ export async function fetchSupplementalContextForTest( return { supplementalContextItems: await generateSupplementalContextFromFocalFile( crossSourceFile, - 'ByName', + 'byName', cancellationToken ), - strategy: 'ByName', + strategy: 'byName', } } throwIfCancelled(cancellationToken) @@ -84,10 +84,10 @@ export async function fetchSupplementalContextForTest( return { supplementalContextItems: await generateSupplementalContextFromFocalFile( crossSourceFile, - 'ByContent', + 'byContent', cancellationToken ), - strategy: 'ByContent', + strategy: 'byContent', } } @@ -95,7 +95,7 @@ export async function fetchSupplementalContextForTest( getLogger().debug(`CodeWhisperer failed to fetch utg context`) return { supplementalContextItems: [], - strategy: 'Empty', + strategy: 'empty', } } diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index a34deb0cdca..964145fd881 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -22,7 +22,7 @@ import { getSelectedCustomization } from './customizationUtil' import { AuthUtil } from './authUtil' import { isAwsError } from '../../shared/errors' import { getLogger } from '../../shared/logger' -import { session } from './codeWhispererSession' +import { CodeWhispererSessionState } from './codeWhispererSession' import { CodeWhispererSupplementalContext } from '../models/model' import { FeatureConfigProvider } from '../../shared/featureConfig' import { CodeScanRemediationsEventType } from '../client/codewhispereruserclient' @@ -70,6 +70,7 @@ export class TelemetryHelper { public sendTestGenerationToolkitEvent( session: Session, isSupportedLanguage: boolean, + isFileInWorkspace: boolean, result: 'Succeeded' | 'Failed' | 'Cancelled', requestId?: string, perfClientLatency?: number, @@ -90,6 +91,7 @@ export class TelemetryHelper { cwsprChatProgrammingLanguage: session.fileLanguage ?? 'plaintext', hasUserPromptSupplied: session.hasUserPromptSupplied, isSupportedLanguage: isSupportedLanguage, + isFileInWorkspace: isFileInWorkspace, result: result, artifactsUploadDuration: artifactsUploadDuration, buildPayloadBytes: buildPayloadBytes, @@ -122,6 +124,7 @@ export class TelemetryHelper { reason: string, supplementalContextMetadata?: CodeWhispererSupplementalContext | undefined ) { + const session = CodeWhispererSessionState.instance.getSession() const event = { codewhispererAutomatedTriggerType: session.autoTriggerType, codewhispererCursorOffset: session.startCursorOffset, @@ -155,6 +158,7 @@ export class TelemetryHelper { supplementalContextMetadata?: CodeWhispererSupplementalContext | undefined ) { const selectedCustomization = getSelectedCustomization() + const session = CodeWhispererSessionState.instance.getSession() telemetry.codewhisperer_userDecision.emit({ codewhispererCompletionType: 'Line', @@ -275,6 +279,7 @@ export class TelemetryHelper { if (_elem.content.length === 0) { recommendationSuggestionState?.set(i, 'Empty') } + const session = CodeWhispererSessionState.instance.getSession() const event: CodewhispererUserDecision = { // TODO: maintain a list of RecommendationContexts with both recommendation and requestId in it, instead of two separate list items. codewhispererCompletionType: this.getCompletionType(i, completionTypes), @@ -336,6 +341,7 @@ export class TelemetryHelper { if (!events.length) { return } + const session = CodeWhispererSessionState.instance.getSession() const aggregated: CodewhispererUserTriggerDecision = { codewhispererAutomatedTriggerType: session.autoTriggerType, codewhispererCompletionType: events[0].codewhispererCompletionType, @@ -385,6 +391,7 @@ export class TelemetryHelper { .map((e) => e.codewhispererSuggestionCount) .reduce((a, b) => a + b, 0) + const session = CodeWhispererSessionState.instance.getSession() const aggregated: CodewhispererUserTriggerDecision = { codewhispererAutomatedTriggerType: autoTriggerType, codewhispererCharactersAccepted: acceptedRecommendationContent.length, @@ -506,6 +513,7 @@ export class TelemetryHelper { } private resetUserTriggerDecisionTelemetry() { + const session = CodeWhispererSessionState.instance.getSession() this.sessionDecisions = [] this.triggerChar = '' this.typeAheadLength = 0 @@ -590,6 +598,7 @@ export class TelemetryHelper { } public resetClientComponentLatencyTime() { + const session = CodeWhispererSessionState.instance.getSession() session.invokeSuggestionStartTime = 0 session.preprocessEndTime = 0 session.sdkApiCallStartTime = 0 @@ -601,6 +610,7 @@ export class TelemetryHelper { } public setPreprocessEndTime() { + const session = CodeWhispererSessionState.instance.getSession() if (session.preprocessEndTime !== 0) { getLogger().warn(`inline completion preprocessEndTime has been set and not reset correctly`) } @@ -609,11 +619,13 @@ export class TelemetryHelper { /** This method is assumed to be invoked first at the start of execution **/ public setInvokeSuggestionStartTime() { + const session = CodeWhispererSessionState.instance.getSession() this.resetClientComponentLatencyTime() session.invokeSuggestionStartTime = performance.now() } public setSdkApiCallEndTime() { + const session = CodeWhispererSessionState.instance.getSession() if (this._sdkApiCallEndTime === 0 && session.sdkApiCallStartTime !== 0) { this._sdkApiCallEndTime = performance.now() } @@ -626,6 +638,7 @@ export class TelemetryHelper { } public setFirstSuggestionShowTime() { + const session = CodeWhispererSessionState.instance.getSession() if (session.firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { session.firstSuggestionShowTime = performance.now() } @@ -640,6 +653,7 @@ export class TelemetryHelper { // report client component latency after all pagination call finish // and at least one suggestion is shown to the user public tryRecordClientComponentLatency() { + const session = CodeWhispererSessionState.instance.getSession() if (session.firstSuggestionShowTime === 0 || this._allPaginationEndTime === 0) { return } diff --git a/packages/core/src/codewhisperer/util/zipUtil.ts b/packages/core/src/codewhisperer/util/zipUtil.ts index ab938aeb643..64a9ccc3b8d 100644 --- a/packages/core/src/codewhisperer/util/zipUtil.ts +++ b/packages/core/src/codewhisperer/util/zipUtil.ts @@ -20,9 +20,10 @@ import { NoSourceFilesError, ProjectSizeExceededError, } from '../models/errors' -import { ZipUseCase } from '../models/constants' +import { FeatureUseCase } from '../models/constants' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' import { removeAnsi } from '../../shared' +import { ProjectZipError } from '../../amazonqTest/error' export interface ZipMetadata { rootDir: string @@ -135,7 +136,7 @@ export class ZipUtil { if (this.reachSizeLimit(this._totalSize, scope)) { throw new FileSizeExceededError() } - const zipFilePath = this.getZipDirPath(ZipUseCase.CODE_SCAN) + CodeWhispererConstants.codeScanZipExt + const zipFilePath = this.getZipDirPath(FeatureUseCase.CODE_SCAN) + CodeWhispererConstants.codeScanZipExt zip.writeZip(zipFilePath) return zipFilePath } @@ -203,15 +204,15 @@ export class ZipUtil { await processDirectory(metadataDir) } - protected async zipProject(useCase: ZipUseCase, projectPath?: string, metadataDir?: string) { + protected async zipProject(useCase: FeatureUseCase, projectPath?: string, metadataDir?: string) { const zip = new admZip() let projectPaths = [] - if (useCase === ZipUseCase.TEST_GENERATION && projectPath) { + if (useCase === FeatureUseCase.TEST_GENERATION && projectPath) { projectPaths.push(projectPath) } else { projectPaths = this.getProjectPaths() } - if (useCase === ZipUseCase.CODE_SCAN) { + if (useCase === FeatureUseCase.CODE_SCAN) { await this.processCombinedGitDiff(zip, projectPaths, '', CodeWhispererConstants.CodeAnalysisScope.PROJECT) } const languageCount = new Map() @@ -220,7 +221,7 @@ export class ZipUtil { if (metadataDir) { await this.processMetadataDir(zip, metadataDir) } - if (useCase !== ZipUseCase.TEST_GENERATION) { + if (useCase !== FeatureUseCase.TEST_GENERATION) { this.processOtherFiles(zip, languageCount) } @@ -403,7 +404,7 @@ export class ZipUtil { zip: admZip, languageCount: Map, projectPaths: string[] | undefined, - useCase: ZipUseCase + useCase: FeatureUseCase ) { if (!projectPaths || projectPaths.length === 0) { return @@ -420,7 +421,7 @@ export class ZipUtil { const zipEntryPath = this.getZipEntryPath(projectName, file.relativeFilePath) if (ZipConstants.knownBinaryFileExts.includes(path.extname(file.fileUri.fsPath))) { - if (useCase === ZipUseCase.TEST_GENERATION) { + if (useCase === FeatureUseCase.TEST_GENERATION) { continue } await this.processBinaryFile(zip, file.fileUri, zipEntryPath) @@ -511,10 +512,10 @@ export class ZipUtil { return vscode.workspace.textDocuments.some((document) => document.uri.fsPath === uri.fsPath && document.isDirty) } - public getZipDirPath(useCase: ZipUseCase): string { + public getZipDirPath(useCase: FeatureUseCase): string { if (this._zipDir === '') { const prefix = - useCase === ZipUseCase.TEST_GENERATION + useCase === FeatureUseCase.TEST_GENERATION ? CodeWhispererConstants.TestGenerationTruncDirPrefix : CodeWhispererConstants.codeScanTruncDirPrefix @@ -528,7 +529,7 @@ export class ZipUtil { scope: CodeWhispererConstants.CodeAnalysisScope ): Promise { try { - const zipDirPath = this.getZipDirPath(ZipUseCase.CODE_SCAN) + const zipDirPath = this.getZipDirPath(FeatureUseCase.CODE_SCAN) let zipFilePath: string if ( scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO || @@ -536,7 +537,7 @@ export class ZipUtil { ) { zipFilePath = await this.zipFile(uri, scope) } else if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { - zipFilePath = await this.zipProject(ZipUseCase.CODE_SCAN) + zipFilePath = await this.zipProject(FeatureUseCase.CODE_SCAN) } else { throw new ToolkitError(`Unknown code analysis scope: ${scope}`) } @@ -562,7 +563,7 @@ export class ZipUtil { public async generateZipTestGen(projectPath: string, initialExecution: boolean): Promise { try { // const repoMapFile = await LspClient.instance.getRepoMapJSON() - const zipDirPath = this.getZipDirPath(ZipUseCase.TEST_GENERATION) + const zipDirPath = this.getZipDirPath(FeatureUseCase.TEST_GENERATION) const metadataDir = path.join(zipDirPath, 'utgRequiredArtifactsDir') @@ -590,7 +591,7 @@ export class ZipUtil { } } - const zipFilePath: string = await this.zipProject(ZipUseCase.TEST_GENERATION, projectPath, metadataDir) + const zipFilePath: string = await this.zipProject(FeatureUseCase.TEST_GENERATION, projectPath, metadataDir) const zipFileSize = (await fs.stat(zipFilePath)).size return { rootDir: zipDirPath, @@ -604,7 +605,9 @@ export class ZipUtil { } } catch (error) { getLogger().error('Zip error caused by: %s', error) - throw error + throw new ProjectZipError( + error instanceof Error ? error.message : 'Unknown error occurred during zip operation' + ) } } // TODO: Refactor this diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index 12a52f9b4eb..39732c84064 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -16,7 +16,7 @@ import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' import { Commands } from '../../shared/vscode/commands2' -import { session } from '../util/codeWhispererSession' +import { CodeWhispererSessionState } from '../util/codeWhispererSession' import { RecommendationHandler } from '../service/recommendationHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { setContext } from '../../shared' @@ -75,6 +75,7 @@ export class AutotriggerState implements AnnotationState { static acceptedCount = 0 updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + const session = CodeWhispererSessionState.instance.getSession() if (AutotriggerState.acceptedCount < RecommendationService.instance.acceptedSuggestionCount) { return new ManualtriggerState() } else if (session.recommendations.length > 0 && RecommendationHandler.instance.isSuggestionVisible()) { diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index 7c1c655a937..d511bd9a5f6 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -27,12 +27,12 @@ export class SecurityIssueWebview extends VueWebview { public readonly onChangeIssue = new vscode.EventEmitter() public readonly onChangeFilePath = new vscode.EventEmitter() public readonly onChangeGenerateFixLoading = new vscode.EventEmitter() - public readonly onChangeGenerateFixError = new vscode.EventEmitter() + public readonly onChangeGenerateFixError = new vscode.EventEmitter() private issue: CodeScanIssue | undefined private filePath: string | undefined private isGenerateFixLoading: boolean = false - private isGenerateFixError: boolean = false + private generateFixError: string | null | undefined = undefined public constructor() { super(SecurityIssueWebview.sourcePath) @@ -99,13 +99,13 @@ export class SecurityIssueWebview extends VueWebview { this.onChangeGenerateFixLoading.fire(isGenerateFixLoading) } - public getIsGenerateFixError() { - return this.isGenerateFixError + public getGenerateFixError() { + return this.generateFixError } - public setIsGenerateFixError(isGenerateFixError: boolean) { - this.isGenerateFixError = isGenerateFixError - this.onChangeGenerateFixError.fire(isGenerateFixError) + public setGenerateFixError(generateFixError: string | null | undefined) { + this.generateFixError = generateFixError + this.onChangeGenerateFixError.fire(generateFixError) } public generateFix() { @@ -201,7 +201,7 @@ export async function showSecurityIssueWebview(ctx: vscode.ExtensionContext, iss activePanel.server.setIssue(issue) activePanel.server.setFilePath(filePath) activePanel.server.setIsGenerateFixLoading(false) - activePanel.server.setIsGenerateFixError(false) + activePanel.server.setGenerateFixError(undefined) const webviewPanel = await activePanel.show({ title: amazonqCodeIssueDetailsTabTitle, @@ -247,7 +247,7 @@ type WebviewParams = { issue?: CodeScanIssue filePath?: string isGenerateFixLoading?: boolean - isGenerateFixError?: boolean + generateFixError?: string | null shouldRefreshView: boolean context: vscode.ExtensionContext } @@ -255,7 +255,7 @@ export async function updateSecurityIssueWebview({ issue, filePath, isGenerateFixLoading, - isGenerateFixError, + generateFixError, shouldRefreshView, context, }: WebviewParams): Promise { @@ -271,8 +271,8 @@ export async function updateSecurityIssueWebview({ if (isGenerateFixLoading !== undefined) { activePanel.server.setIsGenerateFixLoading(isGenerateFixLoading) } - if (isGenerateFixError !== undefined) { - activePanel.server.setIsGenerateFixError(isGenerateFixError) + if (generateFixError !== undefined) { + activePanel.server.setGenerateFixError(generateFixError) } if (shouldRefreshView && filePath && issue) { await showSecurityIssueWebview(context, issue, filePath) diff --git a/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue b/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue index a086aea3089..c28d12a021d 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue +++ b/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue @@ -48,16 +48,14 @@

Suggested code fix preview

-
-                Something went wrong. Retry
-            
+
{{ generateFixError }}
@@ -195,7 +193,7 @@ export default defineComponent({ endLine: 0, relativePath: '', isGenerateFixLoading: false, - isGenerateFixError: false, + generateFixError: undefined as string | null | undefined, languageId: 'plaintext', fixedCode: '', referenceText: '', @@ -218,8 +216,8 @@ export default defineComponent({ const relativePath = await client.getRelativePath() this.updateRelativePath(relativePath) const isGenerateFixLoading = await client.getIsGenerateFixLoading() - const isGenerateFixError = await client.getIsGenerateFixError() - this.updateGenerateFixState(isGenerateFixLoading, isGenerateFixError) + const generateFixError = await client.getGenerateFixError() + this.updateGenerateFixState(isGenerateFixLoading, generateFixError) const languageId = await client.getLanguageId() if (languageId) { this.updateLanguageId(languageId) @@ -249,16 +247,16 @@ export default defineComponent({ this.isGenerateFixLoading = isGenerateFixLoading this.scrollTo('codeFixSection') }) - client.onChangeGenerateFixError((isGenerateFixError) => { - this.isGenerateFixError = isGenerateFixError + client.onChangeGenerateFixError((generateFixError) => { + this.generateFixError = generateFixError }) }, updateRelativePath(relativePath: string) { this.relativePath = relativePath }, - updateGenerateFixState(isGenerateFixLoading: boolean, isGenerateFixError: boolean) { + updateGenerateFixState(isGenerateFixLoading: boolean, generateFixError: string | null | undefined) { this.isGenerateFixLoading = isGenerateFixLoading - this.isGenerateFixError = isGenerateFixError + this.generateFixError = generateFixError }, updateLanguageId(languageId: string) { this.languageId = languageId diff --git a/packages/core/src/codewhisperer/vue/backend.ts b/packages/core/src/codewhisperer/vue/backend.ts index e4baecadc18..f92552e247a 100644 --- a/packages/core/src/codewhisperer/vue/backend.ts +++ b/packages/core/src/codewhisperer/vue/backend.ts @@ -8,7 +8,6 @@ import * as os from 'os' import * as vscode from 'vscode' import * as path from 'path' import { VueWebview } from '../../webviews/main' -import { isCloud9 } from '../../shared/extensionUtilities' import globals from '../../shared/extensionGlobals' import { telemetry, CodewhispererLanguage, CodewhispererGettingStartedTask } from '../../shared/telemetry/telemetry' import { fs } from '../../shared' @@ -148,7 +147,7 @@ export async function showCodeWhispererWebview( } const webview = await activePanel!.show({ title: localize('AWS.view.gettingStartedPage.title', `Learn Amazon Q`), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codewhisperer/vue/genSuggestionTab.vue b/packages/core/src/codewhisperer/vue/genSuggestionTab.vue index 2ab39cc5ffe..b4191e9e73b 100644 --- a/packages/core/src/codewhisperer/vue/genSuggestionTab.vue +++ b/packages/core/src/codewhisperer/vue/genSuggestionTab.vue @@ -68,7 +68,7 @@ export default defineComponent({ { column1: [ 'AmazonQ_generate_suggestion.py', - `# TODO: place your cursor at the end of line 5 and press Enter to generate a suggestion.${'\n'}# Tip: press tab to accept the suggestion.${'\n'}${'\n'}fake_users = [${'\n'} { "name": "User 1", "id": "user1", "city": "San Francisco", "state": "CA" },`, + `# TODO: place your cursor at the end of line 6 and press Enter to generate a suggestion.${'\n'}# Tip: press tab to accept the suggestion.${'\n'}${'\n'}fake_users = [${'\n'} { "name": "User 1", "id": "user1", "city": "San Francisco", "state": "CA" },\n]`, ], column2: [ 'AmazonQ_manual_invoke.py', diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index fc164ebb95c..b849b328bac 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -9,6 +9,7 @@ import * as vscode from 'vscode' import { ToolkitError } from '../../../../shared/errors' import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient' import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDeveloperChatClient' +import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker' export class ChatSession { private sessionId?: string @@ -48,6 +49,7 @@ export class ChatSession { } } + UserWrittenCodeTracker.instance.onQFeatureInvoked() return response } @@ -67,6 +69,8 @@ export class ChatSession { this.sessionId = response.conversationId + UserWrittenCodeTracker.instance.onQFeatureInvoked() + return response } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 57b45d414c1..a5205be78ca 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -231,17 +231,19 @@ export class ChatController { this.openLinkInExternalBrowser(click) } - private processQuickActionCommand(quickActionCommand: ChatPromptCommandType) { + private processQuickActionCommand(message: PromptMessage) { this.editorContextExtractor .extractContextForTrigger('QuickAction') .then((context) => { const triggerID = randomUUID() + const quickActionCommand = message.command as ChatPromptCommandType + this.messenger.sendQuickActionMessage(quickActionCommand, triggerID) this.triggerEventsStorage.addTriggerEvent({ id: triggerID, - tabID: undefined, + tabID: message.tabID, message: undefined, type: 'quick_action', quickAction: quickActionCommand, @@ -484,7 +486,7 @@ export class ChatController { recordTelemetryChatRunCommand('clear') return default: - this.processQuickActionCommand(message.command) + this.processQuickActionCommand(message) } } diff --git a/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts b/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts index d2b80f2619f..d782f7147ff 100644 --- a/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts +++ b/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts @@ -7,7 +7,7 @@ import { TextEditor, Selection, TextDocument, Range } from 'vscode' import { FocusAreaContext, FullyQualifiedName } from './model' -const focusAreaCharLimit = 9_000 +const focusAreaCharLimit = 40_000 export class FocusAreaContextExtractor { public isCodeBlockSelected(editor: TextEditor): boolean { diff --git a/packages/core/src/commands.ts b/packages/core/src/commands.ts index 6177c953242..f1e77f24cf9 100644 --- a/packages/core/src/commands.ts +++ b/packages/core/src/commands.ts @@ -36,7 +36,7 @@ import { CommonAuthWebview } from './login/webview' import { AuthSource, AuthSources } from './login/webview/util' import { ServiceItemId, isServiceItemId } from './login/webview/vue/types' import { authHelpUrl } from './shared/constants' -import { isCloud9, getIdeProperties } from './shared/extensionUtilities' +import { getIdeProperties } from './shared/extensionUtilities' import { telemetry } from './shared/telemetry/telemetry' import { createCommonButtons } from './shared/ui/buttons' import { showQuickPick } from './shared/ui/pickerPrompter' @@ -66,12 +66,7 @@ export function registerCommands(context: vscode.ExtensionContext) { const addConnection = Commands.register( { id: 'aws.toolkit.auth.addConnection', telemetryThrottleMs: false }, async () => { - const c9IamItem = createIamItem() - c9IamItem.detail = - 'Activates working with resources in the Explorer. Requires an access key ID and secret access key.' - const items = isCloud9() - ? [createSsoItem(), c9IamItem] - : [createBuilderIdItem(), createSsoItem(), createIamItem()] + const items = [createBuilderIdItem(), createSsoItem(), createIamItem()] const resp = await showQuickPick(items, { title: localize('aws.auth.addConnection.title', 'Add a Connection to {0}', getIdeProperties().company), @@ -113,9 +108,7 @@ export function registerCommands(context: vscode.ExtensionContext) { source = AuthSources.vscodeComponent } - // The auth webview page does not make sense to use in C9, - // so show the auth quick pick instead. - if (isCloud9('any') || isWeb()) { + if (isWeb()) { // TODO: CW no longer exists in toolkit. This should be moved to Amazon Q if (source.toLowerCase().includes('codewhisperer')) { // Show CW specific quick pick for CW connections diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index b4100b191ce..0033eaa8548 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -24,6 +24,7 @@ import { getSessionId } from '../shared/telemetry/util' import { NotificationsController } from '../notifications/controller' import { DevNotificationsState } from '../notifications/types' import { QuickPickItem } from 'vscode' +import { ChildProcess } from '../shared/utilities/processUtils' interface MenuOption { readonly label: string @@ -44,6 +45,7 @@ export type DevFunction = | 'editAuthConnections' | 'notificationsSend' | 'forceIdeCrash' + | 'startChildProcess' export type DevOptions = { context: vscode.ExtensionContext @@ -126,6 +128,11 @@ const menuOptions: () => Record = () => { detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`, executor: forceQuitIde, }, + startChildProcess: { + label: 'ChildProcess: Start child process', + detail: 'Start ChildProcess from our utility wrapper for testing', + executor: startChildProcess, + }, } } @@ -334,7 +341,7 @@ class ObjectEditor { return this.openState(targetContext.secrets, key) case 'auth': // Auth memento is determined in a different way - return this.openState(getEnvironmentSpecificMemento(), key) + return this.openState(getEnvironmentSpecificMemento(globalState), key) } } @@ -578,3 +585,15 @@ async function editNotifications() { await targetNotificationsController.pollForEmergencies() }) } + +async function startChildProcess() { + const result = await createInputBox({ + title: 'Enter a command', + }).prompt() + if (result) { + const [command, ...args] = result?.toString().split(' ') ?? [] + getLogger().info(`Starting child process: '${command}'`) + const processResult = await ChildProcess.run(command, args, { collect: true }) + getLogger().info(`Child process exited with code ${processResult.exitCode}`) + } +} diff --git a/packages/core/src/dev/beta.ts b/packages/core/src/dev/beta.ts index b09516dcc3c..89bd085e84e 100644 --- a/packages/core/src/dev/beta.ts +++ b/packages/core/src/dev/beta.ts @@ -18,7 +18,7 @@ import { isUserCancelledError, ToolkitError } from '../shared/errors' import { telemetry } from '../shared/telemetry/telemetry' import { cast } from '../shared/utilities/typeConstructors' import { CancellationError } from '../shared/utilities/timeoutUtils' -import { isAmazonQ, isCloud9, productName } from '../shared/extensionUtilities' +import { isAmazonQ, productName } from '../shared/extensionUtilities' import * as devConfig from './config' import { isReleaseVersion } from '../shared/vscode/env' import { getRelativeDate } from '../shared/datetime' @@ -49,7 +49,7 @@ async function updateBetaToolkitData(vsixUrl: string, data: BetaToolkit) { */ export async function activate(ctx: vscode.ExtensionContext) { const betaUrl = isAmazonQ() ? devConfig.betaUrl.amazonq : devConfig.betaUrl.toolkit - if (!isCloud9() && !isReleaseVersion() && betaUrl) { + if (!isReleaseVersion() && betaUrl) { ctx.subscriptions.push(watchBetaVSIX(betaUrl)) } } diff --git a/packages/core/src/docdb/activation.ts b/packages/core/src/docdb/activation.ts new file mode 100644 index 00000000000..23ee206e4f6 --- /dev/null +++ b/packages/core/src/docdb/activation.ts @@ -0,0 +1,125 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Commands } from '../shared' +import { ExtContext } from '../shared/extensions' +import { DBResourceNode } from './explorer/dbResourceNode' +import { DocumentDBNode } from './explorer/docdbNode' +import { DBClusterNode } from './explorer/dbClusterNode' +import { DBInstanceNode } from './explorer/dbInstanceNode' +import { addRegion } from './commands/addRegion' +import { createCluster } from './commands/createCluster' +import { deleteCluster } from './commands/deleteCluster' +import { renameCluster } from './commands/renameCluster' +import { startCluster } from './commands/startCluster' +import { stopCluster } from './commands/stopCluster' +import { createInstance } from './commands/createInstance' +import { deleteInstance } from './commands/deleteInstance' +import { modifyInstance } from './commands/modifyInstance' +import { rebootInstance } from './commands/rebootInstance' +import { renameInstance } from './commands/renameInstance' +import { addTag, listTags, removeTag } from './commands/tagCommands' +import { Uri } from 'vscode' +import { openUrl } from '../shared/utilities/vsCodeUtils' +import { getLogger } from '../shared/logger' + +/** + * A utility function to automatically invoke trackChanges after a command. + */ + +function withTrackChanges( + command: (node: T) => Promise, + commandName: string = 'UnnamedCommand' +): (node: T) => Promise { + return async (node: T) => { + const arn = node.arn || 'UnknownARN' + const startTime = new Date().toISOString() + + getLogger().info( + `[${startTime}] Executing command "${commandName}" for resource with ARN: ${arn}. Tracking changes will be invoked post-execution.` + ) + + await command(node) + + const endTime = new Date().toISOString() + getLogger().info( + `[${endTime}] Successfully executed command "${commandName}" for resource with ARN: ${arn}. Invoking trackChanges now.` + ) + + await node.trackChangesWithWaitProcessingStatus() + } +} + +/** + * Activates DocumentDB components. + */ +export async function activate(ctx: ExtContext): Promise { + ctx.extensionContext.subscriptions.push( + Commands.register('aws.docdb.createCluster', async (node?: DocumentDBNode) => { + await createCluster(node) + }), + + Commands.register('aws.docdb.deleteCluster', withTrackChanges(deleteCluster, 'deleteCluster')), + + Commands.register('aws.docdb.renameCluster', withTrackChanges(renameCluster, 'renameCluster')), + + Commands.register('aws.docdb.startCluster', withTrackChanges(startCluster, 'startCluster')), + + Commands.register('aws.docdb.stopCluster', withTrackChanges(stopCluster, 'stopCluster')), + + Commands.register('aws.docdb.addRegion', withTrackChanges(addRegion, 'addRegion')), + + Commands.register( + 'aws.docdb.createInstance', + withTrackChanges(createInstance, 'createInstance') + ), + + Commands.register( + 'aws.docdb.deleteInstance', + withTrackChanges(deleteInstance, 'deleteInstance') + ), + + Commands.register( + 'aws.docdb.modifyInstance', + withTrackChanges(modifyInstance, 'modifyInstance') + ), + + Commands.register( + 'aws.docdb.rebootInstance', + withTrackChanges(rebootInstance, 'rebootInstance') + ), + + Commands.register( + 'aws.docdb.renameInstance', + withTrackChanges(renameInstance, 'renameInstance') + ), + + Commands.register('aws.docdb.listTags', async (node: DBResourceNode) => { + await listTags(node) + }), + + Commands.register('aws.docdb.addTag', async (node: DBResourceNode) => { + await addTag(node) + }), + + Commands.register('aws.docdb.removeTag', async (node: DBResourceNode) => { + await removeTag(node) + }), + + Commands.register('aws.docdb.viewConsole', async (node?: DBResourceNode) => { + await node?.openInBrowser() + }), + + Commands.register('aws.docdb.viewDocs', async () => { + await openUrl( + Uri.parse('https://docs.aws.amazon.com/documentdb/latest/developerguide/get-started-guide.html') + ) + }), + + Commands.register('aws.docdb.copyEndpoint', async (node?: DBResourceNode) => { + await node?.copyEndpoint() + }) + ) +} diff --git a/packages/core/src/docdb/commands/addRegion.ts b/packages/core/src/docdb/commands/addRegion.ts new file mode 100644 index 00000000000..403778e6c6a --- /dev/null +++ b/packages/core/src/docdb/commands/addRegion.ts @@ -0,0 +1,165 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { DBClusterNode } from '../explorer/dbClusterNode' +import { DBGlobalClusterNode } from '../explorer/dbGlobalClusterNode' +import { DefaultDocumentDBClient } from '../../shared/clients/docdbClient' +import { ToolkitError } from '../../shared' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { isValidResponse } from '../../shared/wizards/wizard' +import { CancellationError } from '../../shared/utilities/timeoutUtils' +import { CreateGlobalClusterWizard } from '../wizards/createGlobalClusterWizard' +import { CreateDBClusterMessage } from '@aws-sdk/client-docdb' +import { createInstancesForCluster } from './createCluster' +import { isSupportedGlobalInstanceClass } from '../utils' + +export async function addRegion(node: DBClusterNode | DBGlobalClusterNode): Promise { + if (!node) { + throw new ToolkitError('No node specified for AddRegion') + } + + return telemetry.docdb_addRegion.run(async () => { + let globalClusterName = undefined + + if (node.cluster.StorageEncrypted) { + void vscode.window.showErrorMessage('Encrypted clusters are not supported') + return + } + + if (node instanceof DBClusterNode) { + if (node.clusterRole !== 'regional') { + void vscode.window.showErrorMessage('Only regional clusters are supported') + return + } + + if (node.cluster.DBClusterMembers?.length === 0) { + void vscode.window.showErrorMessage( + localize( + 'AWS.docdb.addRegion.noInstances', + 'Cluster must have at least one instance to add a region' + ) + ) + throw new ToolkitError('Cluster must have at least one instance to add a region', { cancelled: true }) + } + + const unsupportedInstanceFound = node.instances.find( + (instance) => !isSupportedGlobalInstanceClass(instance.DBInstanceClass!) + ) + + if (unsupportedInstanceFound) { + void vscode.window.showErrorMessage( + localize( + 'AWS.docdb.addRegion.unsupportedInstanceClass', + 'Instance class {0} not supported for global cluster. Upgrade the instances then try again.', + unsupportedInstanceFound.DBInstanceClass + ) + ) + throw new ToolkitError('Instance class not supported for global cluster', { + cancelled: true, + code: 'docdbInstanceClassNotSupported', + }) + } + } else { + globalClusterName = node.cluster.GlobalClusterIdentifier + + if (node.cluster.GlobalClusterMembers!.length > 4) { + void vscode.window.showErrorMessage( + localize('AWS.docdb.addRegion.maxRegions', 'Global clusters can have a maximum of 5 regions') + ) + throw new ToolkitError('Global clusters can have a maximum of 5 regions', { + cancelled: true, + code: 'docdbMaxRegionsInUse', + }) + } + } + + if (!node.isAvailable) { + void vscode.window.showErrorMessage(localize('AWS.docdb.clusterStopped', 'Cluster must be running')) + throw new ToolkitError('Cluster not available', { cancelled: true, code: 'docdbClusterStopped' }) + } + + const wizard = new CreateGlobalClusterWizard(node.regionCode, node.cluster.EngineVersion, node.client, { + initState: { GlobalClusterName: globalClusterName }, + }) + const response = await wizard.run() + + if (!isValidResponse(response)) { + throw new CancellationError('user') + } + + const regionCode = response.RegionCode + let input: CreateDBClusterMessage + let clusterName = response.GlobalClusterName + + try { + if (node instanceof DBClusterNode) { + // Create new global cluster from regional cluster + const primaryCluster = node.cluster + + getLogger().info(`docdb: Creating global cluster: ${clusterName}`) + const globalCluster = await node.client.createGlobalCluster({ + GlobalClusterIdentifier: response.GlobalClusterName, + SourceDBClusterIdentifier: primaryCluster.DBClusterArn, + }) + + input = { + GlobalClusterIdentifier: globalCluster?.GlobalClusterIdentifier, + DBClusterIdentifier: response.Cluster.DBClusterIdentifier, + DeletionProtection: primaryCluster.DeletionProtection, + Engine: primaryCluster.Engine, + EngineVersion: primaryCluster.EngineVersion, + StorageType: primaryCluster.StorageType, + StorageEncrypted: globalCluster?.StorageEncrypted, + } + } else { + // Add secondary cluster to global cluster + const globalCluster = node.cluster + + input = { + GlobalClusterIdentifier: globalClusterName, + DBClusterIdentifier: response.Cluster.DBClusterIdentifier, + DeletionProtection: globalCluster.DeletionProtection, + Engine: globalCluster.Engine, + EngineVersion: globalCluster.EngineVersion, + StorageEncrypted: globalCluster.StorageEncrypted, + } + } + + clusterName = response.Cluster.DBClusterIdentifier + getLogger().info(`docdb: Creating secondary cluster: ${clusterName} in region ${regionCode}`) + + const client = DefaultDocumentDBClient.create(regionCode) + const newCluster = await client.createCluster(input) + + if (response.Cluster.DBInstanceCount) { + await createInstancesForCluster( + client, + clusterName, + response.Cluster.DBInstanceClass, + response.Cluster.DBInstanceCount + ) + } + + getLogger().info('docdb: Created cluster: %O', newCluster) + void vscode.window.showInformationMessage(localize('AWS.docdb.addRegion.success', 'Region added')) + + if (node instanceof DBClusterNode) { + node?.parent.refresh() + } else { + node?.refresh() + } + } catch (e) { + getLogger().error(`docdb: Failed to create cluster ${clusterName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.createCluster.error', 'Failed to create cluster: {0}', clusterName) + ) + throw ToolkitError.chain(e, `Failed to create cluster ${clusterName}`) + } + }) +} diff --git a/packages/core/src/docdb/commands/createCluster.ts b/packages/core/src/docdb/commands/createCluster.ts new file mode 100644 index 00000000000..8d0b9658fc0 --- /dev/null +++ b/packages/core/src/docdb/commands/createCluster.ts @@ -0,0 +1,104 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { DocumentDBNode } from '../explorer/docdbNode' +import { CreateClusterWizard } from '../wizards/createClusterWizard' +import { CreateClusterInput } from '@aws-sdk/client-docdb-elastic' +import { DocDBEngine, DocumentDBClient } from '../../shared/clients/docdbClient' + +/** + * Creates a DocumentDB cluster. + * + * Prompts the user for the cluster name. + * Creates the cluster. + * Refreshes the node. + */ +export async function createCluster(node?: DocumentDBNode) { + getLogger().debug('docdb: CreateCluster called for: %O', node) + + await telemetry.docdb_createCluster.run(async (span) => { + if (!node) { + throw new ToolkitError('No node specified for CreateCluster') + } + + span.record({ awsRegion: node?.client.regionCode }) + const wizard = new CreateClusterWizard(node?.client, {}) + const result = await wizard.run() + + if (!result) { + getLogger().debug('docdb: createCluster cancelled') + throw new ToolkitError('User cancelled createCluster wizard', { cancelled: true }) + } + + const clusterName = result.RegionalCluster?.DBClusterIdentifier ?? result.ElasticCluster?.clusterName + getLogger().info(`docdb: Creating cluster: ${clusterName}`) + let cluster + + try { + if (result.ClusterType === 'elastic') { + cluster = await node.client.createElasticCluster(result.ElasticCluster as CreateClusterInput) + } else { + cluster = await node.client.createCluster(result.RegionalCluster) + + // create instances for cluster + if (cluster && result.RegionalCluster.DBInstanceCount) { + await createInstancesForCluster( + node.client, + clusterName, + result.RegionalCluster.DBInstanceClass, + result.RegionalCluster.DBInstanceCount + ) + } + } + + getLogger().info('docdb: Created cluster: %O', cluster) + void vscode.window.showInformationMessage( + localize('AWS.docdb.createCluster.success', 'Created cluster: {0}', clusterName) + ) + + node?.refresh() + return cluster + } catch (e) { + getLogger().error(`docdb: Failed to create cluster ${clusterName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.createCluster.error', 'Failed to create cluster: {0}', clusterName) + ) + throw ToolkitError.chain(e, `Failed to create cluster ${clusterName}`) + } + }) +} + +export async function createInstancesForCluster( + client: DocumentDBClient, + clusterName: string, + instanceClass: string = 'db.t3.medium', + instanceCount: number +) { + const tasks = [] + + for (let index = 0; index < instanceCount; index++) { + tasks.push( + client.createInstance({ + Engine: DocDBEngine, + DBClusterIdentifier: clusterName, + DBInstanceIdentifier: index === 0 ? clusterName : `${clusterName}${index + 1}`, + DBInstanceClass: instanceClass, + }) + ) + } + + try { + await Promise.all(tasks) + } catch (e) { + throw ToolkitError.chain(e, `Failed to create instance for cluster ${clusterName}`, { + code: 'docdbCreateInstanceForCluster', + }) + } +} diff --git a/packages/core/src/docdb/commands/createInstance.ts b/packages/core/src/docdb/commands/createInstance.ts new file mode 100644 index 00000000000..2387bdecd84 --- /dev/null +++ b/packages/core/src/docdb/commands/createInstance.ts @@ -0,0 +1,85 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { DBClusterNode } from '../explorer/dbClusterNode' +import { CreateInstanceWizard } from '../wizards/createInstanceWizard' +import { CreateDBInstanceMessage } from '@aws-sdk/client-docdb' +import { DocDBEngine, MaxInstanceCount } from '../../shared/clients/docdbClient' + +/** + * Creates a DocumentDB instance. + * + * Prompts the user for the instance name and class. + * Creates the instance. + * Refreshes the cluster node. + */ +export async function createInstance(node: DBClusterNode) { + getLogger().debug('docdb: CreateInstance called for: %O', node) + + await telemetry.docdb_createInstance.run(async () => { + if (!node) { + throw new ToolkitError('No node specified for CreateInstance') + } + + const instances = await node.client.listInstances([node.arn]) + if (instances.length >= MaxInstanceCount) { + void vscode.window.showInformationMessage( + localize('AWS.docdb.createInstance.limitReached', 'Max instances in use') + ) + throw new ToolkitError('Max instances in use', { code: 'documentDBMaxInstancesInUse' }) + } + + const generateInstanceName = (clusterName: string) => + instances.length === 0 ? clusterName : `${clusterName}${++instances.length}` + + const options = { + implicitState: { + DBInstanceIdentifier: generateInstanceName(node.cluster.DBClusterIdentifier ?? ''), + DBInstanceClass: instances[0]?.DBInstanceClass, + }, + } + const wizard = new CreateInstanceWizard(node.regionCode, node.cluster, options, node.client) + + const result = await wizard.run() + + if (!result) { + getLogger().debug('docdb: CreateInstance cancelled') + throw new ToolkitError('User cancelled createInstance wizard', { cancelled: true }) + } + + const instanceName = result.DBInstanceIdentifier + getLogger().info(`docdb: Creating instance: ${instanceName}`) + + try { + const request: CreateDBInstanceMessage = { + Engine: DocDBEngine, + DBClusterIdentifier: node.cluster.DBClusterIdentifier, + DBInstanceIdentifier: result.DBInstanceIdentifier, + DBInstanceClass: result.DBInstanceClass !== '' ? result.DBInstanceClass : undefined, + } + + const instance = await node.createInstance(request) + + getLogger().info('docdb: Created instance: %O', instance) + void vscode.window.showInformationMessage( + localize('AWS.docdb.createInstance.success', 'Creating instance: {0}', instanceName) + ) + + node.refresh() + return instance + } catch (e) { + getLogger().error(`docdb: Failed to create instance ${instanceName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.createInstance.error', 'Failed to create instance: {0}', instanceName) + ) + throw ToolkitError.chain(e, `Failed to create instance ${instanceName}`) + } + }) +} diff --git a/packages/core/src/docdb/commands/deleteCluster.ts b/packages/core/src/docdb/commands/deleteCluster.ts new file mode 100644 index 00000000000..6b538cb8d00 --- /dev/null +++ b/packages/core/src/docdb/commands/deleteCluster.ts @@ -0,0 +1,107 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { DBClusterNode } from '../explorer/dbClusterNode' +import { showQuickPick } from '../../shared/ui/pickerPrompter' +import { formatDate, formatTime } from '../../shared/date' +import { telemetry } from '../../shared/telemetry' +import { DBElasticClusterNode } from '../explorer/dbElasticClusterNode' +import { assertNodeAvailable } from '../utils' + +/** + * Deletes a DocumentDB cluster. + * + * Prompts the user for confirmation, and whether to keep a snapshot + * Deletes the cluster and all instances. + * Refreshes the cluster node. + */ +export async function deleteCluster(node: DBClusterNode | DBElasticClusterNode) { + getLogger().debug('docdb: DeleteCluster called for: %O', node) + + await telemetry.docdb_deleteCluster.run(async (span) => { + assertNodeAvailable(node, 'DeleteCluster') + const clusterName = node.name + const isRegionalCluster = node instanceof DBClusterNode + + if (isRegionalCluster && node.cluster.DeletionProtection) { + void vscode.window.showErrorMessage( + localize( + 'AWS.docdb.deleteCluster.protected', + 'Clusters cannot be deleted while deletion protection is enabled' + ) + ) + throw new ToolkitError('Deletion protection is active', { + cancelled: true, + code: 'docdbDeletionProtectionInUse', + }) + } + + const takeSnapshot = await showQuickPick( + [ + { label: localize('AWS.generic.response.yes', 'Yes'), data: true }, + { label: localize('AWS.generic.response.no', 'No'), data: false }, + ], + { + title: localize( + 'AWS.docdb.deleteCluster.promptSnapshot', + 'Delete Cluster - Keep a snapshot of the data?' + ), + } + ) + + if (takeSnapshot === undefined) { + getLogger().debug('docdb: DeleteCluster cancelled') + throw new ToolkitError('User cancelled deleteCluster wizard', { cancelled: true }) + } + + const isConfirmed = await showConfirmationDialog() + if (!isConfirmed) { + getLogger().debug('docdb: DeleteCluster cancelled') + throw new ToolkitError('User cancelled deleteCluster wizard', { cancelled: true }) + } + + try { + getLogger().debug(`docdb: Deleting cluster: ${clusterName}`) + + let finalSnapshotId: string | undefined = undefined + if (takeSnapshot) { + finalSnapshotId = `${clusterName}-${formatDate()}-${formatTime()}` + } + + const cluster = await node.deleteCluster(finalSnapshotId) + + void vscode.window.showInformationMessage( + localize('AWS.docdb.deleteCluster.success', 'Deleting cluster: {0}', clusterName) + ) + + await node.waitUntilStatusChanged() + node.parent.refresh() + getLogger().info('docdb: Deleted cluster: %O', cluster) + return cluster + } catch (e) { + getLogger().error(`docdb: Failed to delete cluster ${clusterName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.deleteCluster.error', 'Failed to delete cluster: {0}', clusterName) + ) + throw ToolkitError.chain(e, `Failed to delete cluster ${clusterName}`) + } + }) +} + +async function showConfirmationDialog(): Promise { + const prompt = localize('AWS.docdb.deleteCluster.prompt', "Enter 'delete entire cluster' to confirm deletion") + const confirmValue = localize('AWS.docdb.deleteCluster.confirmValue', 'delete entire cluster').toLowerCase() + const confirmationInput = await vscode.window.showInputBox({ + prompt, + placeHolder: confirmValue, + validateInput: (input) => (input?.toLowerCase() !== confirmValue ? prompt : undefined), + }) + + return confirmationInput === confirmValue +} diff --git a/packages/core/src/docdb/commands/deleteInstance.ts b/packages/core/src/docdb/commands/deleteInstance.ts new file mode 100644 index 00000000000..580d50c801c --- /dev/null +++ b/packages/core/src/docdb/commands/deleteInstance.ts @@ -0,0 +1,77 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { DBInstanceNode } from '../explorer/dbInstanceNode' +import { DBClusterNode } from '../explorer/dbClusterNode' +import { telemetry } from '../../shared/telemetry' +import { assertNodeAvailable } from '../utils' + +/** + * Deletes a DocumentDB instance. + * + * Prompts the user for confirmation. + * Deletes the instance. + * Refreshes the parent cluster node. + */ +export async function deleteInstance(node: DBInstanceNode) { + getLogger().debug('docdb: DeleteInstance called for: %O', node) + + await telemetry.docdb_deleteInstance.run(async () => { + assertNodeAvailable(node, 'DeleteInstance') + const parent = node.parent as DBClusterNode + const client = parent.client + const instanceName = node.instance.DBInstanceIdentifier ?? '' + + if (!parent?.isAvailable) { + void vscode.window.showErrorMessage( + localize('AWS.docdb.deleteInstance.clusterStopped', 'Cluster must be started to delete instances') + ) + throw new ToolkitError('Cluster not running', { cancelled: true }) + } + + const isConfirmed = await showConfirmationDialog(instanceName) + if (!isConfirmed) { + getLogger().debug('docdb: DeleteInstance cancelled') + throw new ToolkitError('User cancelled deleteInstance', { cancelled: true }) + } + + try { + getLogger().info(`docdb: Deleting instance: ${instanceName}`) + + const instance = await client.deleteInstance({ + DBInstanceIdentifier: instanceName, + }) + + getLogger().info('docdb: Deleted instance: %O', instance) + void vscode.window.showInformationMessage( + localize('AWS.docdb.deleteInstance.success', 'Deleting instance: {0}', instanceName) + ) + + parent.refresh() + return instance + } catch (e) { + getLogger().error(`docdb: Failed to delete instance ${instanceName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.deleteInstance.error', 'Failed to delete instance: {0}', instanceName) + ) + throw ToolkitError.chain(e, `Failed to delete instance ${instanceName}`) + } + }) +} + +async function showConfirmationDialog(instanceName: string): Promise { + const prompt = localize('AWS.docdb.deleteInstance.prompt', 'Enter {0} to confirm deletion', instanceName) + const confirmationInput = await vscode.window.showInputBox({ + prompt, + placeHolder: instanceName, + validateInput: (input) => (input !== instanceName ? prompt : undefined), + }) + + return confirmationInput === instanceName +} diff --git a/packages/core/src/docdb/commands/modifyInstance.ts b/packages/core/src/docdb/commands/modifyInstance.ts new file mode 100644 index 00000000000..626c1be3d97 --- /dev/null +++ b/packages/core/src/docdb/commands/modifyInstance.ts @@ -0,0 +1,97 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { DBInstanceNode } from '../explorer/dbInstanceNode' +import { DBCluster, ModifyDBInstanceMessage } from '@aws-sdk/client-docdb' +import { DBStorageType, DocumentDBClient } from '../../shared/clients/docdbClient' +import { createQuickPick, DataQuickPickItem } from '../../shared/ui/pickerPrompter' +import { isValidResponse } from '../../shared/wizards/wizard' +import { telemetry } from '../../shared/telemetry' +import { assertNodeAvailable } from '../utils' + +/** + * Modifies a DocumentDB instance. + * + * Prompts the user for the instance class. + * Updates the instance. + * Refreshes the node. + */ +export async function modifyInstance(node: DBInstanceNode) { + getLogger().debug('docdb: ModifyInstance called for: %O', node) + + await telemetry.docdb_resizeInstance.run(async () => { + assertNodeAvailable(node, 'ModifyInstance') + const instanceName = node.instance.DBInstanceIdentifier + const parent = node.parent + + const quickPickItems = await getInstanceClassOptions(parent.client, parent.cluster) + const newInstanceClass = await promptForInstanceClass(quickPickItems, node.instance.DBInstanceClass ?? '') + + if (!newInstanceClass) { + getLogger().debug('docdb: ModifyInstance cancelled') + throw new ToolkitError('User cancelled modifyInstance wizard', { cancelled: true }) + } + + try { + const request: ModifyDBInstanceMessage = { + DBInstanceIdentifier: instanceName, + DBInstanceClass: newInstanceClass, + ApplyImmediately: true, + } + + const instance = await parent.client.modifyInstance(request) + + getLogger().info('docdb: Modified instance: %O', instanceName) + void vscode.window.showInformationMessage( + localize('AWS.docdb.modifyInstance.success', 'Modified instance: {0}', instanceName) + ) + + await node.waitUntilStatusChanged() + parent.refresh() + return instance + } catch (e) { + getLogger().error(`docdb: Failed to modify instance ${instanceName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.modifyInstance.error', 'Failed to modify instance: {0}', instanceName) + ) + throw ToolkitError.chain(e, `Failed to modify instance ${instanceName}`) + } + }) +} + +async function getInstanceClassOptions( + client: DocumentDBClient, + cluster: DBCluster +): Promise[]> { + const options = await client.listInstanceClassOptions( + cluster.EngineVersion, + cluster.StorageType ?? DBStorageType.Standard + ) + + const items: DataQuickPickItem[] = options.map((option) => { + return { + data: option.DBInstanceClass, + label: option.DBInstanceClass ?? '(unknown)', + } + }) + + return items +} + +async function promptForInstanceClass(items: any[], currentValue: string) { + const prompter = createQuickPick(items, { + title: localize('AWS.docdb.createInstance.instanceClass.prompt', 'Select instance class'), + value: currentValue, + }) + + prompter.recentItem = items.find((item) => item.data === currentValue) + + const response = await prompter.prompt() + return isValidResponse(response) ? response : undefined +} diff --git a/packages/core/src/docdb/commands/rebootInstance.ts b/packages/core/src/docdb/commands/rebootInstance.ts new file mode 100644 index 00000000000..43458f4c9a6 --- /dev/null +++ b/packages/core/src/docdb/commands/rebootInstance.ts @@ -0,0 +1,59 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as localizedText from '../../shared/localizedText' +import { getLogger, ToolkitError } from '../../shared' +import { CancellationError } from '../../shared/utilities/timeoutUtils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showConfirmationMessage, showViewLogsMessage } from '../../shared/utilities/messages' +import { telemetry } from '../../shared/telemetry' +import { DBInstanceNode } from '../explorer/dbInstanceNode' +import { assertNodeAvailable } from '../utils' + +/** + * Reboots a DocumentDB instance. + * Refreshes the parent node. + */ +export async function rebootInstance(node: DBInstanceNode) { + getLogger().debug('docdb: RebootInstance called for: %O', node) + + await telemetry.docdb_rebootInstance.run(async () => { + assertNodeAvailable(node, 'RebootInstance') + + const isConfirmed = await showConfirmationMessage({ + prompt: localize( + 'AWS.docdb.rebootInstance.prompt', + 'Are you sure you want to reboot instance {0}?', + node.name + ), + confirm: localizedText.yes, + cancel: localizedText.cancel, + }) + if (!isConfirmed) { + getLogger().debug('docdb: RebootInstance canceled') + throw new CancellationError('user') + } + + const instanceName = node.instance.DBInstanceIdentifier + try { + const instance = await node.rebootInstance() + + getLogger().info('docdb: Rebooting instance: %s', instanceName) + void vscode.window.showInformationMessage( + localize('AWS.docdb.rebootInstance.success', 'Rebooting instance: {0}', instanceName) + ) + + node.parent.refresh() + return instance + } catch (e) { + getLogger().error(`docdb: Failed to reboot instance ${instanceName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.rebootInstance.error', 'Failed to reboot instance: {0}', instanceName) + ) + throw ToolkitError.chain(e, `Failed to reboot instance ${instanceName}`) + } + }) +} diff --git a/packages/core/src/docdb/commands/renameCluster.ts b/packages/core/src/docdb/commands/renameCluster.ts new file mode 100644 index 00000000000..f4067983f6a --- /dev/null +++ b/packages/core/src/docdb/commands/renameCluster.ts @@ -0,0 +1,59 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, sleep, ToolkitError } from '../../shared' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { assertNodeAvailable, validateClusterName } from '../utils' +import { DBClusterNode } from '../explorer/dbClusterNode' +import { DBGlobalClusterNode } from '../explorer/dbGlobalClusterNode' +import { telemetry } from '../../shared/telemetry' + +/** + * Renames a DocumentDB cluster. + * + * Prompts the user for the new cluster name + * Updates the cluster. + * Refreshes the node. + */ +export async function renameCluster(node: DBClusterNode | DBGlobalClusterNode) { + getLogger().debug('docdb: RenameCluster called for: %O', node) + + await telemetry.docdb_renameCluster.run(async () => { + assertNodeAvailable(node, 'RenameCluster') + const clusterName = node.name + + const newClusterName = await vscode.window.showInputBox({ + prompt: localize('AWS.docdb.renameCluster.prompt', 'New cluster name'), + value: clusterName, + validateInput: validateClusterName, + }) + + if (!newClusterName) { + getLogger().debug('docdb: RenameCluster cancelled') + throw new ToolkitError('User cancelled renameCluster', { cancelled: true }) + } + + try { + const cluster = await node.renameCluster(newClusterName) + + getLogger().info('docdb: Renamed cluster: %O', cluster) + void vscode.window.showInformationMessage( + localize('AWS.docdb.renameCluster.success', 'Updated cluster: {0}', clusterName) + ) + + await sleep(1000) // wait for server to update status + node.parent.refresh() + return cluster + } catch (e) { + getLogger().error(`docdb: Failed to rename cluster ${clusterName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.renameCluster.error', 'Failed to rename cluster: {0}', clusterName) + ) + throw ToolkitError.chain(e, `Failed to rename cluster ${clusterName}`) + } + }) +} diff --git a/packages/core/src/docdb/commands/renameInstance.ts b/packages/core/src/docdb/commands/renameInstance.ts new file mode 100644 index 00000000000..e7b26131eb7 --- /dev/null +++ b/packages/core/src/docdb/commands/renameInstance.ts @@ -0,0 +1,57 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger, ToolkitError } from '../../shared' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { assertNodeAvailable, validateInstanceName } from '../utils' +import { DBInstanceNode } from '../explorer/dbInstanceNode' +import { telemetry } from '../../shared/telemetry' + +/** + * Renames a DocumentDB instance. + * + * Prompts the user for the new instance name + * Updates the instance. + * Refreshes the node. + */ +export async function renameInstance(node: DBInstanceNode) { + getLogger().debug('docdb: RenameInstance called for: %O', node) + + await telemetry.docdb_renameInstance.run(async () => { + assertNodeAvailable(node, 'RenameInstance') + const instanceName = node.instance.DBInstanceIdentifier + + const newInstanceName = await vscode.window.showInputBox({ + prompt: localize('AWS.docdb.renameInstance.prompt', 'New instance name'), + value: instanceName, + validateInput: validateInstanceName, + }) + + if (!newInstanceName) { + getLogger().debug('docdb: RenameInstance cancelled') + throw new ToolkitError('User cancelled renameInstance', { cancelled: true }) + } + + try { + const instance = await node.renameInstance(newInstanceName) + + getLogger().info('docdb: Renamed instance: %O', instance) + void vscode.window.showInformationMessage( + localize('AWS.docdb.renameInstance.success', 'Updated instance: {0}', instanceName) + ) + + node.refresh() + return instance + } catch (e) { + getLogger().error(`docdb: Failed to rename instance ${instanceName}: %s`, e) + void showViewLogsMessage( + localize('AWS.docdb.renameInstance.error', 'Failed to rename instance: {0}', instanceName) + ) + throw ToolkitError.chain(e, `Failed to rename instance ${instanceName}`) + } + }) +} diff --git a/packages/core/src/docdb/commands/startCluster.ts b/packages/core/src/docdb/commands/startCluster.ts new file mode 100644 index 00000000000..a1ef83ab7be --- /dev/null +++ b/packages/core/src/docdb/commands/startCluster.ts @@ -0,0 +1,22 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { DBClusterNode } from '../explorer/dbClusterNode' + +export function startCluster(node?: DBClusterNode): Promise { + return telemetry.docdb_startCluster.run(async () => { + if (node?.arn && node?.regionCode) { + await node.client.startCluster(node.arn) + getLogger().info('docdb: Start cluster: %O', node.name) + void vscode.window.showInformationMessage( + localize('AWS.docdb.startCluster.success', 'Starting cluster: {0}', node.name) + ) + } + }) +} diff --git a/packages/core/src/docdb/commands/stopCluster.ts b/packages/core/src/docdb/commands/stopCluster.ts new file mode 100644 index 00000000000..e642a78b58a --- /dev/null +++ b/packages/core/src/docdb/commands/stopCluster.ts @@ -0,0 +1,39 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as localizedText from '../../shared/localizedText' +import { getLogger } from '../../shared/logger' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { CancellationError } from '../../shared/utilities/timeoutUtils' +import { showConfirmationMessage } from '../../shared/utilities/messages' +import { DBClusterNode } from '../explorer/dbClusterNode' + +export function stopCluster(node?: DBClusterNode): Promise { + return telemetry.docdb_stopCluster.run(async () => { + if (node?.arn && node?.regionCode) { + const isConfirmed = await showConfirmationMessage({ + prompt: localize( + 'AWS.docdb.stopCluster.prompt', + 'Are you sure you want to stop cluster {0}?', + node.name + ), + confirm: localizedText.yes, + cancel: localizedText.cancel, + }) + if (!isConfirmed) { + getLogger().debug('docdb: StopCluster cancelled') + throw new CancellationError('user') + } + + await node.client.stopCluster(node.arn) + getLogger().info('docdb: Stop cluster: %O', node.name) + void vscode.window.showInformationMessage( + localize('AWS.docdb.stopCluster.success', 'Stopping cluster: {0}', node.name) + ) + } + }) +} diff --git a/packages/core/src/docdb/commands/tagCommands.ts b/packages/core/src/docdb/commands/tagCommands.ts new file mode 100644 index 00000000000..6265451381c --- /dev/null +++ b/packages/core/src/docdb/commands/tagCommands.ts @@ -0,0 +1,112 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger' +import { telemetry } from '../../shared/telemetry' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { DBResourceNode } from '../explorer/dbResourceNode' +import { DataQuickPickItem, showQuickPick } from '../../shared/ui/pickerPrompter' +import { ToolkitError } from '../../shared' + +export async function listTags(node: DBResourceNode): Promise { + return telemetry.docdb_listTags.run(async () => { + const tagMap = await node.listTags() + const tags = Object.entries(tagMap ?? {}).map(([key, value]) => `• ${key} = ${value}`) + const detail = tags.length ? tags.join('\r\n') : '[No tags assigned]' + + const addCommandText = localize('AWS.docdb.tags.add', 'Add tag...') + const removeCommandText = tags.length ? localize('AWS.docdb.tags.remove', 'Remove...') : '' + const commands = tags.length ? [addCommandText, removeCommandText] : [addCommandText] + + const response = await vscode.window.showInformationMessage( + `Tags for ${node.name}:`, + { modal: true, detail }, + ...commands + ) + + switch (response) { + case addCommandText: + await addTag(node) + break + case removeCommandText: + await removeTag(node) + break + } + }) +} + +export async function addTag(node: DBResourceNode): Promise { + return telemetry.docdb_addTag.run(async () => { + const key = await vscode.window.showInputBox({ + title: 'Add Tag', + prompt: localize('AWS.docdb.tags.add.keyPrompt', 'Enter a key for the new tag'), + validateInput: (input) => validateTag(input, 1, 'key'), + }) + if (key === undefined) { + getLogger().info('docdb: AddTag cancelled') + throw new ToolkitError('User cancelled', { cancelled: true }) + } + + const value = await vscode.window.showInputBox({ + title: 'Add Tag', + prompt: localize('AWS.docdb.tags.add.valuePrompt', 'Enter the value for the new tag (optional)'), + validateInput: (input) => validateTag(input, 0, 'value'), + }) + if (value === undefined) { + getLogger().info('docdb: AddTag cancelled') + throw new ToolkitError('User cancelled', { cancelled: true }) + } + + const tag = { [key.trim()]: value.trim() } + await node.client.addResourceTags({ resourceArn: node.arn, tags: tag }) + getLogger().info('docdb: Added resource tag for: %O', node.name) + void vscode.window.showInformationMessage(localize('AWS.docdb.tags.add.success', 'Tag added')) + }) +} + +export async function removeTag(node: DBResourceNode): Promise { + return telemetry.docdb_removeTag.run(async () => { + const tagMap = await node.listTags() + const items = Object.entries(tagMap ?? {}).map>(([key, value]) => { + return { + data: key, + label: key, + description: value, + } + }) + if (items.length === 0) { + return + } + + const resp = await showQuickPick(items, { + title: localize('AWS.docdb.tags.remove.title', 'Remove a tag from {0}', node.name), + }) + + if (resp === undefined) { + getLogger().info('docdb: RemoveTag cancelled') + throw new ToolkitError('User cancelled', { cancelled: true }) + } + + await node.client.removeResourceTags({ resourceArn: node.arn, tagKeys: [resp] }) + getLogger().info('docdb: Removed resource tag for: %O', node.name) + void vscode.window.showInformationMessage(localize('AWS.docdb.tags.remove.success', 'Tag removed')) + }) +} + +export function validateTag(input: string, minLength: number, name: string): string | undefined { + if (input.trim().length < minLength) { + return localize('AWS.docdb.validateTag.error.invalidLength', `Tag ${name} cannot be blank`) + } + + if (!/^([\p{L}\p{Z}\p{N}\._:/=+\-@]*)$/u.test(input)) { + return localize( + 'AWS.docdb.validateTag.error.invalidCharacters', + `Tag ${name} may only contain unicode letters, digits, whitespace, or one of these symbols: _ . : / = + - @` + ) + } + + return undefined +} diff --git a/packages/core/src/docdb/explorer/dbClusterNode.ts b/packages/core/src/docdb/explorer/dbClusterNode.ts new file mode 100644 index 00000000000..f611d023120 --- /dev/null +++ b/packages/core/src/docdb/explorer/dbClusterNode.ts @@ -0,0 +1,192 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as os from 'os' +import * as vscode from 'vscode' +import { inspect } from 'util' +import { copyToClipboard } from '../../shared/utilities/messages' +import { makeChildrenNodes } from '../../shared/treeview/utils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { telemetry } from '../../shared/telemetry' +import { CreateDBInstanceMessage, DBCluster, ModifyDBClusterMessage } from '@aws-sdk/client-docdb' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { DBResourceNode } from './dbResourceNode' +import { DBInstanceNode } from './dbInstanceNode' +import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import { DBInstance, DocumentDBClient } from '../../shared/clients/docdbClient' +import { DocDBContext } from './docdbContext' +import { toTitleCase } from '../../shared' +import { getAwsConsoleUrl } from '../../shared/awsConsole' +import { getLogger } from '../../shared/logger' + +export type DBClusterRole = 'global' | 'regional' | 'primary' | 'secondary' + +/** + * An AWS Explorer node representing DocumentDB clusters. + * + * Contains instances for a specific cluster as child nodes. + */ +export class DBClusterNode extends DBResourceNode { + override name = this.cluster.DBClusterIdentifier! + override arn = this.cluster.DBClusterArn! + public instances: DBInstance[] = [] + private childNodes: DBInstanceNode[] = [] + + constructor( + public readonly parent: AWSTreeNodeBase, + readonly cluster: DBCluster, + client: DocumentDBClient, + readonly clusterRole: DBClusterRole = 'regional' + ) { + super(client, cluster.DBClusterIdentifier ?? '[Cluster]', vscode.TreeItemCollapsibleState.Collapsed) + getLogger().debug(`NEW DBClusterNode: ${cluster.DBClusterArn}`) + this.arn = cluster.DBClusterArn ?? '' + this.name = cluster.DBClusterIdentifier ?? '' + this.contextValue = this.getContext() + this.iconPath = new vscode.ThemeIcon( + this.isAvailable ? 'layers-active' : this.isStopped ? 'layers-dot' : 'loading~spin' + ) + this.description = this.getDescription() + this.tooltip = `${this.name}${os.EOL}Engine: ${this.cluster.EngineVersion}${os.EOL}Status: ${this.cluster.Status}` + if (this.isStatusRequiringPolling()) { + getLogger().debug(`${this.arn} requires polling.`) + this.trackChanges() + } else { + getLogger().debug(`${this.arn} does NOT require polling.`) + } + } + + public override async getChildren(): Promise { + getLogger().debug(`DBClusterNode.getChildren() called`) + return telemetry.docdb_listInstances.run(async () => { + return await makeChildrenNodes({ + getChildNodes: async () => { + this.instances = (await this.client.listInstances([this.arn])).map((i) => { + const member = this.cluster.DBClusterMembers?.find( + (m) => m.DBInstanceIdentifier === i.DBInstanceIdentifier + ) + return { ...i, ...member } + }) + const nodes = this.instances.map((instance) => new DBInstanceNode(this, instance)) + this.childNodes = nodes + return nodes + }, + getNoChildrenPlaceholderNode: async () => { + const title = localize('AWS.explorerNode.docdb.addInstance', 'Add instance...') + const placeholder = new PlaceholderNode(this, title) + placeholder.contextValue = 'awsDocDB.placeholder' + placeholder.command = { title, command: 'aws.docdb.createInstance', arguments: [this] } + return placeholder + }, + sort: (item1, item2) => item1.name.localeCompare(item2.name), + }) + }) + } + + private getContext() { + const context = `${DocDBContext.Cluster}-${this.clusterRole}` + if (this.isAvailable) { + return `${context}-running` + } else if (this.isStopped) { + return `${context}-stopped` + } + return context + } + + public getDescription(): string | boolean { + const role = toTitleCase(this.clusterRole) + if (!this.isAvailable) { + return `${role} cluster • ${toTitleCase(this.status ?? ' ')}` + } + return `${role} cluster` + } + + public async createInstance(request: CreateDBInstanceMessage): Promise { + return await this.client.createInstance(request) + } + + public async renameCluster(clusterName: string): Promise { + const request: ModifyDBClusterMessage = { + DBClusterIdentifier: this.cluster.DBClusterIdentifier, + NewDBClusterIdentifier: clusterName, + ApplyImmediately: true, + } + const response = await this.client.modifyCluster(request) + this.name = response?.DBClusterIdentifier ?? this.name + return response + } + + public async deleteCluster(finalSnapshotId: string | undefined): Promise { + const instances = await this.client.listInstances([this.arn]) + + const tasks = [] + for (const instance of instances) { + tasks.push( + this.client.deleteInstance({ + DBInstanceIdentifier: instance.DBInstanceIdentifier, + }) + ) + } + await Promise.all(tasks) + + return await this.client.deleteCluster({ + DBClusterIdentifier: this.cluster.DBClusterIdentifier, + FinalDBSnapshotIdentifier: finalSnapshotId, + SkipFinalSnapshot: finalSnapshotId === undefined, + }) + } + + override get status() { + return this.cluster.Status + } + + override async getStatus() { + const clusters = await this.client.listClusters(this.arn) + const cluster = clusters[0] + + if (!cluster) { + getLogger().warn(`No cluster found for ARN: ${this.arn}`) + return undefined + } + + getLogger().debug(`Get Status: status ${cluster.Status} for cluster ${this.arn}`) + + this.cluster.Status = cluster.Status + return cluster.Status + } + + override getConsoleUrl() { + return getAwsConsoleUrl('docdb', this.regionCode).with({ + fragment: `cluster-details/${this.name}`, + }) + } + + override copyEndpoint() { + if (this.cluster.Endpoint) { + return copyToClipboard(this.cluster.Endpoint, this.name) + } + return Promise.reject() + } + + override refreshTree(): void { + getLogger().debug(`(DBClusterNode) Refreshing tree for instance: ${this.arn}`) + this.refresh() + this.parent.refresh() + } + + override clearTimer(): void { + this.pollingSet.delete(this.arn) + this.pollingSet.clearTimer() + for (const node of this.childNodes) { + getLogger().debug(`(clearTimer) Removing Polling from node: ${node.arn}`) + node.pollingSet.delete(node.arn) + node.pollingSet.clearTimer() + } + } + + public override [inspect.custom](): string { + return 'DBClusterNode' + } +} diff --git a/packages/core/src/docdb/explorer/dbElasticClusterNode.ts b/packages/core/src/docdb/explorer/dbElasticClusterNode.ts new file mode 100644 index 00000000000..9534c405f63 --- /dev/null +++ b/packages/core/src/docdb/explorer/dbElasticClusterNode.ts @@ -0,0 +1,117 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { inspect } from 'util' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { DBResourceNode } from './dbResourceNode' +import { DBElasticCluster, DocumentDBClient } from '../../shared/clients/docdbClient' +import { DocDBContext } from './docdbContext' +import { copyToClipboard } from '../../shared/utilities/messages' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { getAwsConsoleUrl } from '../../shared/awsConsole' +import { getLogger } from '../../shared/logger' + +/** + * An AWS Explorer node representing DocumentDB elastic clusters. + */ +export class DBElasticClusterNode extends DBResourceNode { + override name = this.cluster.clusterName! + override arn = this.cluster.clusterArn! + + constructor( + public readonly parent: AWSTreeNodeBase, + public cluster: DBElasticCluster, + client: DocumentDBClient + ) { + super(client, cluster.clusterName ?? '[Cluster]', vscode.TreeItemCollapsibleState.None) + this.contextValue = this.getContext() + this.iconPath = new vscode.ThemeIcon( + this.isAvailable ? 'layers-active' : this.isStopped ? 'layers-dot' : 'loading~spin' + ) + this.description = this.getDescription() + this.tooltip = `${this.name}\nStatus: ${this.status}` + if (this.isStatusRequiringPolling()) { + getLogger().debug(`${this.arn} requires polling.`) + this.trackChanges() + } else { + getLogger().debug(`${this.arn} does NOT require polling.`) + } + } + + private getContext() { + if (this.isAvailable) { + return `${DocDBContext.ElasticCluster}-running` + } else if (this.isStopped) { + return `${DocDBContext.ElasticCluster}-stopped` + } + return DocDBContext.ElasticCluster + } + + public getDescription(): string | boolean { + if (!this.isAvailable) { + return `Elastic cluster • ${this.status}` + } + return 'Elastic cluster' + } + + public async deleteCluster(finalSnapshotId: string | undefined): Promise { + if (finalSnapshotId !== undefined) { + void vscode.window.showInformationMessage( + localize('AWS.docdb.deleteCluster.snapshot', 'Taking snapshot of cluster: {0}', this.name) + ) + + await this.client.createClusterSnapshot({ + clusterArn: this.cluster.clusterArn, + snapshotName: finalSnapshotId, + }) + } + return await this.client.deleteElasticCluster(this.arn) + } + + override get status() { + return this.cluster.status?.toLowerCase() + } + + override async getStatus() { + const cluster = await this.client.getElasticCluster(this.arn) + getLogger().debug(`Get Status: status ${cluster?.status} for elastic-cluster ${this.arn}`) + this.cluster.status = cluster?.status + return cluster?.status?.toLowerCase() + } + + override get isAvailable() { + return this.status === 'active' + } + + override getConsoleUrl() { + return getAwsConsoleUrl('docdb', this.regionCode).with({ + fragment: `elastic-cluster-details/${this.arn}`, + }) + } + + override async copyEndpoint() { + // get the full cluster record if we don't have it already + if (this.cluster.clusterEndpoint === undefined) { + this.cluster = (await this.client.getElasticCluster(this.arn)) ?? this.cluster + } + await copyToClipboard(this.cluster.clusterEndpoint!, this.name) + } + + override clearTimer(): void { + this.pollingSet.delete(this.arn) + this.pollingSet.clearTimer() + } + + override refreshTree(): void { + getLogger().debug(`(DBElasticClusterNode) Refreshing tree for instance: ${this.arn}`) + this.refresh() + this.parent.refresh() + } + + public override [inspect.custom](): string { + return 'DBElasticClusterNode' + } +} diff --git a/packages/core/src/docdb/explorer/dbGlobalClusterNode.ts b/packages/core/src/docdb/explorer/dbGlobalClusterNode.ts new file mode 100644 index 00000000000..fcebe18e3f8 --- /dev/null +++ b/packages/core/src/docdb/explorer/dbGlobalClusterNode.ts @@ -0,0 +1,167 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { inspect } from 'util' +import { makeChildrenNodes } from '../../shared/treeview/utils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { telemetry } from '../../shared/telemetry' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import { DBClusterNode, DBClusterRole } from './dbClusterNode' +import { DefaultDocumentDBClient, DocumentDBClient } from '../../shared/clients/docdbClient' +import { DBCluster, GlobalCluster, GlobalClusterMember, ModifyGlobalClusterMessage } from '@aws-sdk/client-docdb' +import { DBResourceNode } from './dbResourceNode' +import { DocDBContext } from './docdbContext' +import { copyToClipboard } from '../../shared/utilities/messages' +import { getAwsConsoleUrl } from '../../shared/awsConsole' +import { getLogger } from '../../shared/logger' + +function getRegionFromArn(arn: string) { + const match = arn.match(/:rds:([^:]+):.*:cluster:/) + return match?.at(1) +} + +/** + * An AWS Explorer node representing DocumentDB global clusters. + * + * Contains regional clusters of a global cluster as child nodes. + */ +export class DBGlobalClusterNode extends DBResourceNode { + override name = this.cluster.GlobalClusterIdentifier! + override arn = this.cluster.GlobalClusterArn! + readonly clusterRole: DBClusterRole = 'global' + + constructor( + public readonly parent: AWSTreeNodeBase, + readonly cluster: GlobalCluster, + private readonly clusterMap: Map, + client: DocumentDBClient + ) { + super(client, cluster.GlobalClusterIdentifier ?? '[Cluster]', vscode.TreeItemCollapsibleState.Collapsed) + this.arn = cluster.GlobalClusterArn ?? '' + this.name = cluster.GlobalClusterIdentifier ?? '' + this.contextValue = this.getContext() + this.iconPath = new vscode.ThemeIcon('globe') // TODO: determine icon for global cluster + this.description = 'Global cluster' + this.tooltip = `${this.name}\nEngine: ${this.cluster.EngineVersion}\nStatus: ${this.cluster.Status} (read-only)` + if (this.isStatusRequiringPolling()) { + getLogger().debug(`${this.arn} requires polling.`) + this.trackChanges() + } else { + getLogger().debug(`${this.arn} does NOT require polling.`) + } + } + + public override async getChildren(): Promise { + return telemetry.docdb_listInstances.run(async () => { + return await makeChildrenNodes({ + getChildNodes: async () => { + const members = this.cluster.GlobalClusterMembers ?? [] + await this.getMemberClusters(members) + + const nodes = members.map((member) => { + const memberRole: DBClusterRole = member.IsWriter ? 'primary' : 'secondary' + const [cluster, client] = this.clusterMap.get(member.DBClusterArn!) ?? [] + + return new DBClusterNode(this, cluster!, client!, memberRole) + }) + + return nodes + }, + getNoChildrenPlaceholderNode: async () => + new PlaceholderNode(this, localize('AWS.explorerNode.docdb.noClusters', '[No Clusters found]')), + sort: (item1, item2) => { + if (item1.clusterRole === 'primary') { + return -1 + } + if (item2.clusterRole === 'primary') { + return 1 + } + return item1.name.localeCompare(item2.name) + }, + }) + }) + } + + private getContext() { + if (this.isAvailable) { + return `${DocDBContext.GlobalCluster}-running` + } else if (this.status === 'stopped') { + return `${DocDBContext.GlobalCluster}-stopped` + } + return DocDBContext.GlobalCluster + } + + // retrieve member cluster details from other regions + private async getMemberClusters(members: GlobalClusterMember[]): Promise { + await Promise.all( + members.map(async (member) => { + if (!this.clusterMap.has(member.DBClusterArn!)) { + const regionCode = getRegionFromArn(member.DBClusterArn!) + if (regionCode) { + const client = DefaultDocumentDBClient.create(regionCode) + const [cluster] = await client.listClusters(member.DBClusterArn!) + this.clusterMap.set(member.DBClusterArn!, [cluster, client]) + } + } + }) + ) + } + + public async renameCluster(clusterName: string): Promise { + const request: ModifyGlobalClusterMessage = { + GlobalClusterIdentifier: this.cluster.GlobalClusterIdentifier, + NewGlobalClusterIdentifier: clusterName, + } + const response = await this.client.modifyGlobalCluster(request) + this.name = response?.GlobalClusterIdentifier ?? this.name + return response + } + + override get status() { + return this.cluster.Status + } + + override async getStatus() { + const client = DefaultDocumentDBClient.create(this.regionCode) + const clusters = await client.listClusters(this.arn) + const cluster = clusters[0] + + if (!cluster) { + getLogger().warn(`No global cluster found for ARN: ${this.arn}`) + return undefined + } + + getLogger().debug(`Get Status: status ${cluster.Status} for global cluster ${this.arn}`) + this.cluster.Status = cluster.Status + return cluster.Status + } + + override copyEndpoint(): Promise { + return copyToClipboard(this.cluster.GlobalClusterResourceId!, this.name) + } + + override getConsoleUrl(): vscode.Uri { + return getAwsConsoleUrl('docdb', this.regionCode).with({ + fragment: `global-cluster-details/${this.name}`, + }) + } + + override refreshTree(): void { + getLogger().debug(`(DBGlobalClusterNode) Refreshing tree for instance: ${this.arn}`) + this.refresh() + this.parent.refresh() + } + + override clearTimer(): void { + this.pollingSet.delete(this.arn) + this.pollingSet.clearTimer() + } + + public override [inspect.custom](): string { + return 'DBGlobalClusterNode' + } +} diff --git a/packages/core/src/docdb/explorer/dbInstanceNode.ts b/packages/core/src/docdb/explorer/dbInstanceNode.ts new file mode 100644 index 00000000000..041640ce562 --- /dev/null +++ b/packages/core/src/docdb/explorer/dbInstanceNode.ts @@ -0,0 +1,119 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { inspect } from 'util' +import { DBInstance } from '../../shared/clients/docdbClient' +import { DocDBContext, DocDBNodeContext } from './docdbContext' +import { DBResourceNode } from './dbResourceNode' +import { DBClusterNode } from './dbClusterNode' +import { ModifyDBInstanceMessage } from '@aws-sdk/client-docdb' +import { copyToClipboard } from '../../shared/utilities/messages' +import { toTitleCase } from '../../shared' +import { getAwsConsoleUrl } from '../../shared/awsConsole' +import { getLogger } from '../../shared/logger' + +/** + * An AWS Explorer node representing a DocumentDB instance. + */ +export class DBInstanceNode extends DBResourceNode { + override name = this.instance.DBInstanceIdentifier! + override arn = this.instance.DBInstanceArn! + + constructor( + public readonly parent: DBClusterNode, + readonly instance: DBInstance + ) { + super(parent.client, instance.DBInstanceIdentifier ?? '[Instance]', vscode.TreeItemCollapsibleState.None) + getLogger().debug(`NEW DBInstanceNode: ${instance.DBInstanceArn}`) + this.description = this.makeDescription() + this.contextValue = this.getContext() + this.iconPath = this.isAvailable || this.isStopped ? undefined : new vscode.ThemeIcon('loading~spin') + this.tooltip = `${this.name}\nClass: ${this.instance.DBInstanceClass}\nStatus: ${this.status}` + getLogger().debug(`Parent of ${instance.DBInstanceArn} is ${parent.arn}`) + if (this.isStatusRequiringPolling()) { + getLogger().debug(`${instance.DBInstanceArn} requires polling.`) + this.trackChanges() + } else { + getLogger().debug(`${instance.DBInstanceArn} does NOT require polling.`) + } + } + + public override isStatusRequiringPolling(): boolean { + const instanceRequiresPolling = super.isStatusRequiringPolling() + const parentRequiresPolling = this.parent.isStatusRequiringPolling() + const requiresPolling = instanceRequiresPolling || parentRequiresPolling + + getLogger().debug( + `isStatusRequiringPolling (DBInstanceNode): Instance ${this.arn} requires polling: ${instanceRequiresPolling}, Parent ${this.parent.arn} requires polling: ${parentRequiresPolling}, Combined result: ${requiresPolling}` + ) + + return requiresPolling + } + + private makeDescription(): string { + const type = this.instance.IsClusterWriter ? 'Primary' : 'Replica' + if (this.getContext() !== DocDBContext.InstanceAvailable) { + return `${toTitleCase(this.status ?? ' ')} ${type} instance` + } + return `${type} instance • ${this.instance.DBInstanceClass}` + } + + private getContext(): DocDBNodeContext { + if (this.isAvailable) { + return DocDBContext.InstanceAvailable + } + return DocDBContext.Instance + } + + public async rebootInstance(): Promise { + const client = this.parent.client + return await client.rebootInstance(this.instance.DBInstanceIdentifier!) + } + + public async renameInstance(instanceName: string): Promise { + const request: ModifyDBInstanceMessage = { + DBInstanceIdentifier: this.instance.DBInstanceIdentifier, + NewDBInstanceIdentifier: instanceName, + ApplyImmediately: true, + } + return await this.parent.client.modifyInstance(request) + } + + override get status() { + return this.instance.DBInstanceStatus + } + + override async getStatus() { + const instance = await this.parent.client.getInstance(this.instance.DBInstanceIdentifier!) + this.instance.DBInstanceStatus = instance?.DBInstanceStatus + return instance?.DBInstanceStatus + } + + override getConsoleUrl() { + return getAwsConsoleUrl('docdb', this.regionCode).with({ + fragment: `instance-details/${this.name}`, + }) + } + + override copyEndpoint() { + return copyToClipboard(this.instance.Endpoint?.Address ?? '', this.name) + } + + override refreshTree(): void { + getLogger().debug(`(DBInstanceNode) Refreshing tree for instance: ${this.arn}`) + this.refresh() + this.parent.refreshTree() + } + + override clearTimer(): void { + this.pollingSet.delete(this.arn) + this.pollingSet.clearTimer() + } + + public override [inspect.custom](): string { + return 'DBInstanceNode' + } +} diff --git a/packages/core/src/docdb/explorer/dbResourceNode.ts b/packages/core/src/docdb/explorer/dbResourceNode.ts new file mode 100644 index 00000000000..a9ded2bea70 --- /dev/null +++ b/packages/core/src/docdb/explorer/dbResourceNode.ts @@ -0,0 +1,175 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { inspect } from 'util' +import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { DocumentDBClient } from '../../shared/clients/docdbClient' +import { waitUntil } from '../../shared' +import { getLogger } from '../../shared/logger' +import { PollingSet } from '../../shared/utilities/pollingSet' + +/** An AWS Explorer node representing a DocumentDB resource. */ +export abstract class DBResourceNode extends AWSTreeNodeBase implements AWSResourceNode { + public override readonly regionCode: string + public abstract readonly arn: string + public abstract readonly name: string + public readonly pollingSet: PollingSet = new PollingSet(30000, this.updateNodeStatus.bind(this)) + private static readonly globalPollingArns: Set = new Set() + public processingStatuses = new Set([ + 'creating', + 'modifying', + 'rebooting', + 'starting', + 'stopping', + 'renaming', + ]) + + protected constructor( + public readonly client: DocumentDBClient, + label: string, + collapsibleState?: vscode.TreeItemCollapsibleState + ) { + super(label, collapsibleState) + this.regionCode = client.regionCode + getLogger().debug(`NEW DBResourceNode`) + } + + public isStatusRequiringPolling(): boolean { + const currentStatus = this.status?.toLowerCase() + const isProcessingStatus = currentStatus !== undefined && this.processingStatuses.has(currentStatus) + getLogger().debug( + `isStatusRequiringPolling (DBResourceNode):: Checking if status "${currentStatus}" for ARN: ${this.arn} requires polling: ${isProcessingStatus}` + ) + return isProcessingStatus + } + + public [inspect.custom](): string { + return 'DBResourceNode' + } + + public abstract get status(): string | undefined + + public abstract getStatus(): Promise + + public abstract refreshTree(): void + + public abstract clearTimer(): void + + public get isAvailable(): boolean { + return this.status === 'available' + } + + public get isStopped(): boolean { + return this.status === 'stopped' + } + + public get isPolling(): boolean { + const isPolling = DBResourceNode.globalPollingArns.has(this.arn) + getLogger().debug(`isPolling: ARN ${this.arn} is ${isPolling ? '' : 'not '}being polled.`) + return isPolling + } + + public set isPolling(value: boolean) { + if (value) { + if (!this.isPolling) { + DBResourceNode.globalPollingArns.add(this.arn) + getLogger().info(`Polling started for ARN: ${this.arn}`) + } else { + getLogger().info(`Polling already active for ARN: ${this.arn}`) + } + } else { + if (this.isPolling) { + DBResourceNode.globalPollingArns.delete(this.arn) + getLogger().info(`Polling stopped for ARN: ${this.arn}`) + } else { + getLogger().info(`Polling was not active for ARN: ${this.arn}`) + } + } + } + + public async waitUntilStatusChanged( + checkProcessingStatuses: boolean = false, + timeout: number = 1200000, + interval: number = 5000 + ): Promise { + await waitUntil( + async () => { + const status = await this.getStatus() + if (checkProcessingStatuses) { + const isProcessingStatus = status !== undefined && this.processingStatuses.has(status.toLowerCase()) + getLogger().debug('docdb: waitUntilStatusChangedToProcessingStatus: %O', isProcessingStatus) + return isProcessingStatus + } else { + const hasStatusChanged = status !== this.status + getLogger().debug('docdb: waitUntilStatusChanged (status): %O', hasStatusChanged) + return hasStatusChanged + } + }, + { timeout, interval, truthy: true } + ) + this.refreshTree() + return false + } + + public async trackChangesWithWaitProcessingStatus() { + getLogger().debug( + `Preparing to track changes with waiting a processing status for ARN: ${this.arn}; condition: ${this.isPolling};` + ) + if (!this.isPolling) { + this.isPolling = true + await this.waitUntilStatusChanged(true, 60000, 1000) + getLogger().debug(`Tracking changes for a processing status wait is over`) + this.pollingSet.add(this.arn) + getLogger().debug(`Tracking changes for ARN: ${this.arn}; condition: ${this.isPolling};`) + } else { + getLogger().debug(`ARN: ${this.arn} already being tracked`) + } + } + + public trackChanges() { + getLogger().debug(`Preparing to track immediately for ARN: ${this.arn}; condition: ${this.isPolling};`) + if (!this.isPolling) { + this.isPolling = true + this.pollingSet.add(this.arn) + getLogger().debug(`Tracking changes for ARN: ${this.arn}; condition: ${this.isPolling};`) + } else { + getLogger().debug(`ARN: ${this.arn} already being tracked`) + } + } + + public async listTags() { + return await this.client.listResourceTags(this.arn) + } + + public abstract copyEndpoint(): Promise + + public abstract getConsoleUrl(): vscode.Uri + + public openInBrowser() { + return vscode.env.openExternal(this.getConsoleUrl()) + } + + private async updateNodeStatus() { + const currentStatus = this.status + const newStatus = await this.getStatus() + getLogger().debug( + `docdb: ${this.arn} updateNodeStatus (new status): ${newStatus} (old status): ${currentStatus}` + ) + if (currentStatus !== newStatus) { + getLogger().info(`docdb: ${this.arn} status: ${newStatus}, refreshing UI`) + this.refreshTree() + } + if (!this.isStatusRequiringPolling()) { + getLogger().info(`docdb: ${this.arn} status: ${newStatus}, refreshing UI`) + getLogger().debug(`pollingSet delete ${this.arn} updateNodeStatus`) + this.pollingSet.delete(this.arn) + this.pollingSet.clearTimer() + this.isPolling = false + this.refreshTree() + } + } +} diff --git a/packages/core/src/docdb/explorer/docdbContext.ts b/packages/core/src/docdb/explorer/docdbContext.ts new file mode 100644 index 00000000000..23e2b2fbbad --- /dev/null +++ b/packages/core/src/docdb/explorer/docdbContext.ts @@ -0,0 +1,14 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const DocDBContext = { + Cluster: 'awsDocDB-cluster', + ElasticCluster: 'awsDocDB-cluster-elastic', + GlobalCluster: 'awsDocDB-cluster-global', + Instance: 'awsDocDB-instance', + InstanceAvailable: 'awsDocDB-instance-available', +} as const + +export type DocDBNodeContext = (typeof DocDBContext)[keyof typeof DocDBContext] diff --git a/packages/core/src/docdb/explorer/docdbNode.ts b/packages/core/src/docdb/explorer/docdbNode.ts new file mode 100644 index 00000000000..18ebed2d0e1 --- /dev/null +++ b/packages/core/src/docdb/explorer/docdbNode.ts @@ -0,0 +1,88 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { inspect } from 'util' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import { makeChildrenNodes } from '../../shared/treeview/utils' +import { DBElasticCluster, DocumentDBClient } from '../../shared/clients/docdbClient' +import { DBClusterNode } from './dbClusterNode' +import { DBElasticClusterNode } from './dbElasticClusterNode' +import { telemetry } from '../../shared/telemetry' +import { DBGlobalClusterNode } from './dbGlobalClusterNode' +import { DBCluster } from '@aws-sdk/client-docdb' +import { getLogger } from '../../shared/logger' +import { DBResourceNode } from './dbResourceNode' + +/** + * An AWS Explorer node representing DocumentDB. + * + * Contains clusters for a specific region as child nodes. + */ +export class DocumentDBNode extends AWSTreeNodeBase { + public override readonly regionCode: string + + public constructor(public readonly client: DocumentDBClient) { + super('DocumentDB', vscode.TreeItemCollapsibleState.Collapsed) + this.contextValue = 'awsDocDBNode' + this.regionCode = client.regionCode + } + + public override async getChildren(): Promise { + return telemetry.docdb_listClusters.run(async () => { + return await makeChildrenNodes({ + getChildNodes: () => { + return this.getClusterNodes() + }, + getNoChildrenPlaceholderNode: async () => + new PlaceholderNode(this, localize('AWS.explorerNode.docdb.noClusters', '[No Clusters found]')), + sort: (item1, item2) => item1.name.localeCompare(item2.name), + }) + }) + } + + private async getClusterNodes() { + const [globalClusters, clusters, elasticClusters] = await Promise.all([ + this.client.listGlobalClusters(), + this.client.listClusters(), + this.client.listElasticClusters(), + ]) + + // contains clusters that are part of a global cluster + const globalClusterMap = new Map() + + for (const globalCluster of globalClusters) { + for (const member of globalCluster.GlobalClusterMembers ?? []) { + const match = clusters.find((c) => c.DBClusterArn === member.DBClusterArn) + if (match?.DBClusterArn) { + globalClusterMap.set(match.DBClusterArn, [match, this.client]) + } + } + } + + // contains clusters that are not part of a global cluster + const regionalClusters = clusters.filter((c) => !globalClusterMap.has(c.DBClusterArn!)) + + const nodes: DBResourceNode[] = [] + nodes.push( + ...globalClusters.map((cluster) => new DBGlobalClusterNode(this, cluster, globalClusterMap, this.client)) + ) + getLogger().info(`Repopulating child regional clusters...`) + nodes.push(...regionalClusters.map((cluster) => new DBClusterNode(this, cluster, this.client))) + nodes.push( + ...elasticClusters.map( + (cluster) => new DBElasticClusterNode(this, cluster as DBElasticCluster, this.client) + ) + ) + + return nodes + } + + public [inspect.custom](): string { + return 'DocumentDBNode' + } +} diff --git a/packages/core/src/docdb/utils.ts b/packages/core/src/docdb/utils.ts new file mode 100644 index 00000000000..44e90089c41 --- /dev/null +++ b/packages/core/src/docdb/utils.ts @@ -0,0 +1,154 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { ToolkitError } from '../shared' +import { localize } from '../shared/utilities/vsCodeUtils' +import { DBInstanceNode } from './explorer/dbInstanceNode' +import { DBResourceNode } from './explorer/dbResourceNode' + +/** + * Validates a cluster name for the CreateCluster API. + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/docdb/command/CreateDBClusterCommand/ + * @returns undefined if the name passes validation. Otherwise, an error message is returned. + */ +export function validateClusterName(name: string): string | undefined { + if (name.length < 1 || name.length > 63) { + return localize( + 'AWS.docdb.validateClusterName.error.invalidLength', + 'Cluster name must be between 1 and 63 characters long' + ) + } + + if (!/^[a-z]/.test(name)) { + return localize( + 'AWS.docdb.validateClusterName.error.invalidStart', + 'Cluster name must start with a lowercase letter' + ) + } + + if (/-$/.test(name) || /--/.test(name)) { + return localize( + 'AWS.docdb.validateClusterName.error.invalidEnd', + 'Cluster name cannot end with a hyphen or contain 2 consecutive hyphens' + ) + } + + if (!/^[a-z0-9\-]+$/.test(name)) { + return localize( + 'AWS.docdb.validateClusterName.error.invalidCharacters', + 'Cluster name must only contain lowercase letters, numbers, and hyphens' + ) + } + + return undefined +} + +/** + * Validates a username + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/docdb/command/CreateDBClusterCommand/ + * @returns undefined if the name passes validation. Otherwise, an error message is returned. + */ +export function validateUsername(name: string): string | undefined { + if (name.length < 1 || name.length > 63) { + return localize( + 'AWS.docdb.validateUsername.error.invalidLength', + 'Username name must be between 1 and 63 characters long' + ) + } + + if (!/^[a-zA-Z]/.test(name)) { + return localize('AWS.docdb.validateUsername.error.invalidStart', 'Username must start with a letter') + } + + if (!/^[a-zA-Z0-9]+$/.test(name)) { + return localize( + 'AWS.docdb.validateUsername.error.invalidCharacters', + 'Username must only contain letters and numbers' + ) + } + + return undefined +} + +/** + * Validates a password + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/docdb/command/CreateDBClusterCommand/ + * @returns undefined if validation passes. Otherwise, an error message is returned. + */ +export function validatePassword(password: string): string | undefined { + if (password.length < 8 || password.length > 100) { + return localize( + 'AWS.docdb.validatePassword.error.invalidLength', + 'Password must be between 8 and 100 characters long' + ) + } + + if (/["\/\@]/.test(password) || !/^[ -~]*$/.test(password)) { + return localize( + 'AWS.docdb.validatePassword.error.invalidCharacters', + 'Password must only contain printable ASCII characters (except for slash, double quotes and @ symbol)' + ) + } + + return undefined +} + +/** + * Validates an instance name for the CreateInstance API. + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/docdb/command/CreateDBInstanceCommand/ + * @returns undefined if the name passes validation. Otherwise, an error message is returned. + */ +export function validateInstanceName(name: string): string | undefined { + if (name.length < 1 || name.length > 63) { + return localize( + 'AWS.docdb.validateInstanceName.error.invalidLength', + 'Instance name must be between 1 and 63 characters long' + ) + } + + if (!/^[a-z]/.test(name)) { + return localize( + 'AWS.docdb.validateInstanceName.error.invalidStart', + 'Instance name must start with a lowercase letter' + ) + } + + if (/-$/.test(name) || /--/.test(name)) { + return localize( + 'AWS.docdb.validateInstanceName.error.invalidEnd', + 'Instance name cannot end with a hyphen or contain 2 consecutive hyphens' + ) + } + + if (!/^[a-z0-9\-]+$/.test(name)) { + return localize( + 'AWS.docdb.validateInstanceName.error.invalidCharacters', + 'Instance name must only contain lowercase letters, numbers, and hyphens' + ) + } + + return undefined +} + +export function isSupportedGlobalInstanceClass(instanceClass: string) { + return /(t3|t4g|r4)/.test(instanceClass) === false +} + +export function assertNodeAvailable(node: DBResourceNode | undefined, action: string) { + if (!node) { + throw new ToolkitError(`No node specified for ${action}`) + } + + if (!node.isAvailable) { + if (node instanceof DBInstanceNode) { + void vscode.window.showErrorMessage(localize('AWS.docdb.instanceStopped', 'Instance must be running')) + throw new ToolkitError('Instance not running', { cancelled: true, code: 'docdbInstanceNotAvailable' }) + } + + void vscode.window.showErrorMessage(localize('AWS.docdb.clusterStopped', 'Cluster must be running')) + throw new ToolkitError('Cluster not running', { cancelled: true, code: 'docdbClusterStopped' }) + } +} diff --git a/packages/core/src/docdb/wizards/createClusterWizard.ts b/packages/core/src/docdb/wizards/createClusterWizard.ts new file mode 100644 index 00000000000..5e81dd89c63 --- /dev/null +++ b/packages/core/src/docdb/wizards/createClusterWizard.ts @@ -0,0 +1,82 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createCommonButtons } from '../../shared/ui/buttons' +import { createExitPrompter } from '../../shared/ui/common/exitPrompter' +import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { Wizard, WizardOptions } from '../../shared/wizards/wizard' +import { RegionalClusterConfiguration, RegionalClusterWizard } from './regionalClusterWizard' +import { ElasticClusterConfiguration, ElasticClusterWizard } from './elasticClusterWizard' +import { DocumentDBClient } from '../../shared/clients/docdbClient' + +const DocDBClusterHelpUrl = + 'https://docs.aws.amazon.com/documentdb/latest/developerguide/docdb-using-elastic-clusters.html' + +type ClusterType = 'regional' | 'elastic' | undefined + +export interface CreateClusterState { + ClusterType: ClusterType + readonly RegionalCluster: RegionalClusterConfiguration + readonly ElasticCluster: ElasticClusterConfiguration +} + +/** + * A wizard to prompt configuration of a new cluster + */ +export class CreateClusterWizard extends Wizard { + title: string + constructor( + readonly client: DocumentDBClient, + options: WizardOptions = {} + ) { + super({ + initState: options.initState, + implicitState: options.implicitState, + exitPrompterProvider: createExitPrompter, + }) + this.title = localize('AWS.docdb.createCluster.title', 'Create DocumentDB Cluster') + } + + public override async init(): Promise { + this.form.ClusterType.bindPrompter(() => createClusterTypePrompter()) + + const regionalClusterWizard = await new RegionalClusterWizard(this.client, this.title).init() + this.form.RegionalCluster.applyBoundForm(regionalClusterWizard.boundForm, { + showWhen: (state) => state.ClusterType === 'regional', + }) + + const elasticClusterWizard = new ElasticClusterWizard(this.client, this.title) + this.form.ElasticCluster.applyBoundForm(elasticClusterWizard.boundForm, { + showWhen: (state) => state.ClusterType === 'elastic', + }) + + return this + } +} + +function createClusterTypePrompter() { + const regionalType: DataQuickPickItem = { + data: 'regional', + label: localize('AWS.docdb.createCluster.clusterType.regional.label', 'Instance Based Cluster'), + detail: localize( + 'AWS.docdb.createCluster.clusterType.regional.detail', + 'Instance based cluster can scale your database to millions of reads per second and up to 128 TiB of storage capacity. With instance based clusters you can choose your instance type based on your requirements.' + ), + } + const elasticType: DataQuickPickItem = { + data: 'elastic', + label: localize('AWS.docdb.createCluster.clusterType.elastic.label', 'Elastic Cluster'), + detail: localize( + 'AWS.docdb.createCluster.clusterType.elastic.detail', + 'Elastic clusters can scale your database to millions of reads and writes per second, with petabytes of storage capacity. Elastic clusters support MongoDB compatible sharding APIs. With Elastic Clusters, you do not need to choose, manage or upgrade instances.' + ), + } + + return createQuickPick([regionalType, elasticType], { + title: localize('AWS.docdb.createCluster.clusterType.prompt', 'Cluster type'), + buttons: createCommonButtons(DocDBClusterHelpUrl), + }) +} diff --git a/packages/core/src/docdb/wizards/createGlobalClusterWizard.ts b/packages/core/src/docdb/wizards/createGlobalClusterWizard.ts new file mode 100644 index 00000000000..e177047fcb4 --- /dev/null +++ b/packages/core/src/docdb/wizards/createGlobalClusterWizard.ts @@ -0,0 +1,74 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { globals } from '../../shared' +import { DocDBEngine, DocumentDBClient } from '../../shared/clients/docdbClient' +import { createExitPrompter } from '../../shared/ui/common/exitPrompter' +import { createRegionPrompter } from '../../shared/ui/common/region' +import { createInputBox } from '../../shared/ui/inputPrompter' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { Wizard, WizardOptions } from '../../shared/wizards/wizard' +import { validateClusterName } from '../utils' +import { RegionalClusterConfiguration, RegionalClusterWizard } from './regionalClusterWizard' + +const DocDBGlobalHelpUrl = 'https://docs.aws.amazon.com/documentdb/latest/developerguide/global-clusters.html' + +export interface CreateGlobalClusterState { + RegionCode: string + GlobalClusterName: string + readonly Cluster: RegionalClusterConfiguration +} + +/** + * A wizard to prompt configuration of a new global cluster + */ +export class CreateGlobalClusterWizard extends Wizard { + constructor( + readonly region: string, + readonly engineVersion: string | undefined, + readonly client: DocumentDBClient, + options: WizardOptions = {} + ) { + super({ + initState: options.initState, + implicitState: options.implicitState, + exitPrompterProvider: createExitPrompter, + }) + } + + public override async init(): Promise { + this.form.RegionCode.bindPrompter(async () => { + const regions = globals.regionProvider.getRegions().filter((r) => r.id !== this.region) + return createRegionPrompter(regions, { + serviceFilter: DocDBEngine, + title: localize('AWS.docdb.addRegion.region.prompt', 'Secondary region'), + helpUrl: DocDBGlobalHelpUrl, + }).transform((region) => region.id) + }) + + this.form.GlobalClusterName.bindPrompter( + () => + createInputBox({ + title: localize('AWS.docdb.addRegion.name.title', 'Global Cluster Id'), + prompt: localize( + 'AWS.docdb.addRegion.name.prompt', + 'Specify a unique identifier for the global cluster' + ), + validateInput: validateClusterName, + }), + { + showWhen: (state) => state.GlobalClusterName === undefined, + } + ) + + const title = localize('AWS.docdb.addRegion.cluster.title', 'Secondary cluster') + const regionalClusterWizard = await new RegionalClusterWizard(this.client, title, false, { + initState: { EngineVersion: this.engineVersion }, + }).init() + this.form.Cluster.applyBoundForm(regionalClusterWizard.boundForm) + + return this + } +} diff --git a/packages/core/src/docdb/wizards/createInstanceWizard.ts b/packages/core/src/docdb/wizards/createInstanceWizard.ts new file mode 100644 index 00000000000..8bf5b952e02 --- /dev/null +++ b/packages/core/src/docdb/wizards/createInstanceWizard.ts @@ -0,0 +1,96 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DBCluster, OrderableDBInstanceOption } from '@aws-sdk/client-docdb' +import { DefaultDocumentDBClient, DocumentDBClient, DBStorageType } from '../../shared/clients/docdbClient' +import { validateInstanceName } from '../utils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { Wizard, WizardOptions } from '../../shared/wizards/wizard' +import { createInputBox } from '../../shared/ui/inputPrompter' +import { createExitPrompter } from '../../shared/ui/common/exitPrompter' +import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter' +import { createCommonButtons } from '../../shared/ui/buttons' +import { SkipPrompter } from '../../shared/ui/common/skipPrompter' + +const DocDBHelpUrl = 'https://docs.aws.amazon.com/documentdb/latest/developerguide/db-instances.html' + +export interface CreateInstanceState { + DBInstanceIdentifier: string + DBInstanceClass: string +} + +/** + * A wizard to prompt configuration of a new instance + */ +export class CreateInstanceWizard extends Wizard { + title: string + cluster: DBCluster + constructor( + region: string, + cluster: DBCluster, + options: WizardOptions = {}, + readonly client: DocumentDBClient = DefaultDocumentDBClient.create(region) + ) { + super({ + initState: { + ...options.initState, + }, + implicitState: options.implicitState, + exitPrompterProvider: createExitPrompter, + }) + this.cluster = cluster + this.client = client + this.title = localize('AWS.docdb.createInstance.title', 'Add Instance') + } + + public override async init(): Promise { + const form = this.form + + form.DBInstanceIdentifier.bindPrompter(() => + createInputBox({ + step: 1, + title: this.title, + prompt: localize('AWS.docdb.createInstance.name.prompt', 'Instance Name'), + placeholder: localize('AWS.docdb.createInstance.name.placeholder', 'Specify a unique identifier'), + validateInput: validateInstanceName, + }) + ) + + form.DBInstanceClass.bindPrompter(async (state) => await this.createInstanceClassPrompter(state.stepCache)) + + return this + } + + private async createInstanceClassPrompter(cache: { [key: string]: any }) { + const cachedOptions: OrderableDBInstanceOption[] = cache[this.client.regionCode] + const options = + cachedOptions ?? + (await this.client.listInstanceClassOptions( + this.cluster.EngineVersion, + this.cluster.StorageType ?? DBStorageType.Standard + )) + cache[this.client.regionCode] = options + + const items: DataQuickPickItem[] = options.map((option) => { + return { + data: option.DBInstanceClass, + label: option.DBInstanceClass ?? '(unknown)', + description: undefined, + detail: undefined, + recentlyUsed: false, + } + }) + + if (items.length === 0) { + return new SkipPrompter() + } + + return createQuickPick(items, { + step: 2, + title: localize('AWS.docdb.createInstance.instanceClass.prompt', 'Select instance class'), + buttons: createCommonButtons(DocDBHelpUrl), + }) + } +} diff --git a/packages/core/src/docdb/wizards/elasticClusterWizard.ts b/packages/core/src/docdb/wizards/elasticClusterWizard.ts new file mode 100644 index 00000000000..4dca7c99b51 --- /dev/null +++ b/packages/core/src/docdb/wizards/elasticClusterWizard.ts @@ -0,0 +1,132 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DocumentDBClient } from '../../shared/clients/docdbClient' +import { validateClusterName, validatePassword, validateUsername } from '../utils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { Wizard } from '../../shared/wizards/wizard' +import { createInputBox } from '../../shared/ui/inputPrompter' +import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter' +import { createCommonButtons } from '../../shared/ui/buttons' +import { Auth, CreateClusterInput } from '@aws-sdk/client-docdb-elastic' +import { Prompter } from '../../shared' + +const DocDBElasticHelpUrl = 'https://docs.aws.amazon.com/documentdb/latest/developerguide/elastic-how-it-works.html' +const DefaultShardCount = 2 +const DefaultCapacity = 2 + +export interface ElasticClusterConfiguration extends Partial {} + +/** + * A wizard to prompt configuration of a new cluster + */ +export class ElasticClusterWizard extends Wizard { + constructor( + readonly client: DocumentDBClient, + readonly title: string + ) { + super() + this.client = client + const form = this.form + + form.clusterName.bindPrompter(() => + createInputBox({ + title: this.title, + prompt: localize('AWS.docdb.createCluster.name.prompt', 'Specify a unique cluster name'), + validateInput: validateClusterName, + buttons: createCommonButtons(DocDBElasticHelpUrl), + }) + ) + + form.adminUserName.bindPrompter(() => createUsernamePrompter(this.title)) + form.adminUserPassword.bindPrompter(() => createPasswordPrompter(this.title)) + form.authType.setDefault(() => Auth.PLAIN_TEXT) + form.shardCount.bindPrompter(() => createShardCountPrompter(this.title), { + setDefault: () => DefaultShardCount, + }) + form.shardInstanceCount.bindPrompter( + () => + createQuickPick(instanceCountItems(2), { + title: localize( + 'AWS.docdb.createCluster.dbInstanceCount.prompt', + 'The number of replica instances applying to all shards in the elastic cluster' + ), + buttons: createCommonButtons(DocDBElasticHelpUrl), + }), + { setDefault: () => 2 } + ) + form.shardCapacity.bindPrompter(() => createShardCapacityPrompter(this.title), { + setDefault: () => DefaultCapacity, + }) + + return this + } +} + +function createUsernamePrompter(title: string) { + return createInputBox({ + title, + prompt: localize('AWS.docdb.createCluster.username.prompt', 'Specify a login username'), + validateInput: validateUsername, + buttons: createCommonButtons(DocDBElasticHelpUrl), + }) +} + +function createPasswordPrompter(title: string) { + return createInputBox({ + title, + prompt: localize('AWS.docdb.createCluster.password.prompt', 'Specify a login password (8 characters minimum)'), + password: true, + validateInput: validatePassword, + buttons: createCommonButtons(DocDBElasticHelpUrl), + }) +} + +function createShardCountPrompter(title: string): Prompter { + const maxShardCount = 32 + const prompter = createInputBox({ + title, + prompt: localize('AWS.docdb.createCluster.shardCount.prompt', 'Number of shards the Elastic Cluster will use'), + validateInput: (value) => { + const num = parseInt(value) + if (num < 1 || num > maxShardCount || isNaN(num)) { + return localize( + 'AWS.docdb.createCluster.shardCount.invalidValue', + `Enter a numeric value between 1 and ${maxShardCount}` + ) + } + return undefined + }, + buttons: createCommonButtons(DocDBElasticHelpUrl), + }) + return prompter.transform((value) => parseInt(value)) +} + +function createShardCapacityPrompter(title: string): Prompter { + const items = [2, 4, 8, 16, 32, 64].map>((data) => ({ + data, + label: data.toString(), + })) + return createQuickPick(items, { + title, + placeholder: localize('AWS.docdb.createCluster.shardCapacity.placeholder', 'vCPU capacity of shard instances'), + buttons: createCommonButtons(DocDBElasticHelpUrl), + }) +} + +function instanceCountItems(defaultCount: number, max: number = 16): DataQuickPickItem[] { + const items = [] + for (let i = 1; i <= max; i++) { + const item: DataQuickPickItem = { + label: i.toString(), + data: i, + description: i === defaultCount ? '(recommended)' : undefined, + } + + items.push(item) + } + + return items +} diff --git a/packages/core/src/docdb/wizards/regionalClusterWizard.ts b/packages/core/src/docdb/wizards/regionalClusterWizard.ts new file mode 100644 index 00000000000..069c4f6d36a --- /dev/null +++ b/packages/core/src/docdb/wizards/regionalClusterWizard.ts @@ -0,0 +1,206 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CreateDBClusterCommandInput } from '@aws-sdk/client-docdb' +import { DBStorageType, DocDBEngine, DocumentDBClient, MaxInstanceCount } from '../../shared/clients/docdbClient' +import { isSupportedGlobalInstanceClass, validateClusterName, validatePassword, validateUsername } from '../utils' +import { localize } from '../../shared/utilities/vsCodeUtils' +import { Wizard, WizardOptions } from '../../shared/wizards/wizard' +import { createInputBox } from '../../shared/ui/inputPrompter' +import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter' +import { createCommonButtons } from '../../shared/ui/buttons' +import { SkipPrompter } from '../../shared/ui/common/skipPrompter' + +const DocDBClusterHelpUrl = 'https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-parameters.html' + +export interface RegionalClusterConfiguration extends CreateDBClusterCommandInput { + DBClusterIdentifier: string + // These options cannot be changed later + EngineVersion: string + MasterUsername: string + MasterUserPassword: string + StorageEncrypted: boolean + KmsKeyId?: string | undefined + DBSubnetGroupName?: string | undefined + VpcSecurityGroupIds?: string[] | undefined + // Instance fields + DBInstanceCount?: number | undefined + DBInstanceClass?: string | undefined +} + +/** + * A wizard to prompt configuration of a new cluster + */ +export class RegionalClusterWizard extends Wizard { + constructor( + readonly client: DocumentDBClient, + readonly title: string, + readonly isPrimaryCluster: boolean = true, + options: WizardOptions = {} + ) { + super(options) + this.client = client + } + + public override async init(): Promise { + const form = this.form + + form.DBClusterIdentifier.bindPrompter( + () => + createInputBox({ + step: 1, + title: this.title, + prompt: localize('AWS.docdb.createCluster.name.prompt', 'Specify a unique cluster name'), + validateInput: validateClusterName, + buttons: createCommonButtons(DocDBClusterHelpUrl), + }), + { relativeOrder: 1 } + ) + + form.Engine.setDefault(() => DocDBEngine) + form.EngineVersion.bindPrompter(async () => await createEngineVersionPrompter(this.client), { + showWhen: () => this.isPrimaryCluster, + setDefault: () => this.options.initState?.EngineVersion, + }) + form.MasterUsername.bindPrompter(() => createUsernamePrompter(this.title), { + showWhen: () => this.isPrimaryCluster, + }) + form.MasterUserPassword.bindPrompter(() => createPasswordPrompter(this.title), { + showWhen: () => this.isPrimaryCluster, + }) + form.StorageEncrypted.bindPrompter(() => createEncryptedStoragePrompter(), { + showWhen: () => this.isPrimaryCluster, + }) + + form.DBInstanceCount.bindPrompter( + () => + createQuickPick(instanceCountItems(3), { + title: localize('AWS.docdb.createCluster.dbInstanceCount.prompt', 'Number of instances'), + buttons: createCommonButtons(DocDBClusterHelpUrl), + }), + { + setDefault: () => 3, + } + ) + + form.DBInstanceClass.bindPrompter( + async (state) => + await createInstanceClassPrompter(this.client, state.EngineVersion!, this.isPrimaryCluster), + { + setDefault: () => 'db.t3.medium', + showWhen: (state) => state.DBInstanceCount! > 0, + } + ) + + return this + } +} + +function createUsernamePrompter(title: string) { + return createInputBox({ + step: 3, + title, + prompt: localize('AWS.docdb.createCluster.username.prompt', 'Specify a login username'), + validateInput: validateUsername, + buttons: createCommonButtons(DocDBClusterHelpUrl), + }) +} + +function createPasswordPrompter(title: string) { + return createInputBox({ + step: 4, + title, + prompt: localize('AWS.docdb.createCluster.password.prompt', 'Specify a login password (8 characters minimum)'), + password: true, + validateInput: validatePassword, + buttons: createCommonButtons(DocDBClusterHelpUrl), + }) +} + +function createEncryptedStoragePrompter() { + return createQuickPick( + [ + { + label: localize('AWS.docdb.createCluster.storage.encrypted', 'Encrypt'), + description: '(recommended)', + data: true, + }, + { + label: localize('AWS.docdb.createCluster.storage.notEncrypted', "Don't encrypt"), + data: false, + }, + ], + { + title: localize('AWS.docdb.createCluster.storageEncrypted.prompt', 'Specify storage encryption'), + buttons: createCommonButtons(DocDBClusterHelpUrl), + } + ) +} + +async function createEngineVersionPrompter(docdbClient: DocumentDBClient) { + const versions = await docdbClient.listEngineVersions() + // sort in descending order + versions.sort((a, b) => b.EngineVersion!.localeCompare(a.EngineVersion!)) + + const items: DataQuickPickItem[] = versions.map((v) => { + return { + label: v.EngineVersion ?? '', + data: v.EngineVersion, + } + }) + + if (items.length === 0) { + items.push({ label: '5.0.0 (default)', data: '5.0.0' }) + } + + items[0].picked = true + + return createQuickPick(items, { + title: localize('AWS.docdb.createCluster.engineVersion.prompt', 'Select engine version'), + buttons: createCommonButtons(DocDBClusterHelpUrl), + }) +} + +async function createInstanceClassPrompter( + docdbClient: DocumentDBClient, + engineVersion: string, + isPrimaryCluster: boolean +) { + const options = await docdbClient.listInstanceClassOptions(engineVersion, DBStorageType.Standard) + + const items: DataQuickPickItem[] = options + .filter((option) => isPrimaryCluster || isSupportedGlobalInstanceClass(option.DBInstanceClass!)) + .map((option) => ({ + data: option.DBInstanceClass, + label: option.DBInstanceClass ?? '(unknown)', + description: undefined, + detail: undefined, + })) + + if (items.length === 0) { + return new SkipPrompter() + } + + return createQuickPick(items, { + title: localize('AWS.docdb.createInstance.instanceClass.prompt', 'Select instance class'), + buttons: createCommonButtons(DocDBClusterHelpUrl), + }) +} + +// TODO: Make this it's own picker class +function instanceCountItems(defaultCount: number, max: number = MaxInstanceCount): DataQuickPickItem[] { + const items = [] + + for (let index = 1; index <= max; index++) { + const item: DataQuickPickItem = { + label: index.toString(), + data: index, + description: index === defaultCount ? '(recommended)' : undefined, + } + items.push(item) + } + + return items +} diff --git a/packages/core/src/dynamicResources/awsResourceManager.ts b/packages/core/src/dynamicResources/awsResourceManager.ts index a6045559675..de990e6c6b9 100644 --- a/packages/core/src/dynamicResources/awsResourceManager.ts +++ b/packages/core/src/dynamicResources/awsResourceManager.ts @@ -17,7 +17,6 @@ import { getLogger } from '../shared/logger/logger' import { getTabSizeSetting } from '../shared/utilities/editorUtilities' import { ResourceNode } from './explorer/nodes/resourceNode' import { ResourceTypeNode } from './explorer/nodes/resourceTypeNode' -import { isCloud9 } from '../shared/extensionUtilities' import globals from '../shared/extensionGlobals' import { fs } from '../shared' @@ -72,7 +71,7 @@ export class AwsResourceManager { } const doc = await vscode.workspace.openTextDocument(uri) - if (existing && !isCloud9()) { + if (existing) { await this.close(existing) } diff --git a/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts b/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts index 72374d17a5b..313ce4e7d2a 100644 --- a/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts +++ b/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts @@ -14,7 +14,6 @@ import { ResourceTypeNode } from './resourceTypeNode' import { CloudFormation } from 'aws-sdk' import { CloudControlClient, DefaultCloudControlClient } from '../../../shared/clients/cloudControlClient' import { memoizedGetResourceTypes, ResourceTypeMetadata } from '../../model/resources' -import { isCloud9 } from '../../../shared/extensionUtilities' import { ResourcesSettings } from '../../commands/configure' const localize = nls.loadMessageBundle() @@ -57,8 +56,7 @@ export class ResourcesNode extends AWSTreeNodeBase { public async updateChildren(): Promise { const resourceTypes = memoizedGetResourceTypes() - const defaultResources = isCloud9() ? Array.from(resourceTypes.keys()) : [] - const enabledResources = this.settings.get('enabledResources', defaultResources) + const enabledResources = this.settings.get('enabledResources', []) // Use the most recently update type definition per-type const types = await toArrayAsync(this.cloudFormation.listTypes()) diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 00fd730b490..4a57f96e8fe 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -17,7 +17,7 @@ import globals, { initialize, isWeb } from './shared/extensionGlobals' import { join } from 'path' import { Commands } from './shared/vscode/commands2' import { endpointsFileUrl, githubCreateIssueUrl, githubUrl } from './shared/constants' -import { getIdeProperties, aboutExtension, isCloud9, getDocUrl } from './shared/extensionUtilities' +import { getIdeProperties, aboutExtension, getDocUrl } from './shared/extensionUtilities' import { logAndShowError, logAndShowWebviewError } from './shared/utilities/logAndShowUtils' import { telemetry } from './shared/telemetry/telemetry' import { openUrl } from './shared/utilities/vsCodeUtils' @@ -38,7 +38,6 @@ import { RegionProvider, getEndpointsFromFetcher } from './shared/regions/region import { getMachineId, isAutomation } from './shared/vscode/env' import { registerCommandErrorHandler } from './shared/vscode/commands2' import { registerWebviewErrorHandler } from './webviews/server' -import { showQuickStartWebview } from './shared/extensionStartup' import { ExtContext, VSCODE_EXTENSION_ID } from './shared/extensions' import { getSamCliContext } from './shared/sam/cli/samCliContext' import { UriHandler } from './shared/vscode/uriHandler' @@ -99,19 +98,6 @@ export async function activateCommon( void maybeShowMinVscodeWarning('1.83.0') - if (isCloud9()) { - vscode.window.withProgress = wrapWithProgressForCloud9(globals.outputChannel) - context.subscriptions.push( - Commands.register('aws.quickStart', async () => { - try { - await showQuickStartWebview(context) - } finally { - telemetry.aws_helpQuickstart.emit({ result: 'Succeeded' }) - } - }) - ) - } - // setup globals globals.machineId = await getMachineId() globals.awsContext = new DefaultAwsContext() @@ -204,12 +190,12 @@ export function registerGenericCommands(extensionContext: vscode.ExtensionContex * https://docs.aws.amazon.com/general/latest/gr/rande.html */ export function makeEndpointsProvider() { - let localManifestFetcher: ResourceFetcher - let remoteManifestFetcher: ResourceFetcher + let localManifestFetcher: ResourceFetcher + let remoteManifestFetcher: ResourceFetcher if (isWeb()) { localManifestFetcher = { get: async () => JSON.stringify(endpoints) } // Cannot use HttpResourceFetcher due to web mode breaking on import - remoteManifestFetcher = { get: async () => (await fetch(endpointsFileUrl)).text() } + remoteManifestFetcher = { get: async () => await fetch(endpointsFileUrl) } } else { localManifestFetcher = new FileResourceFetcher(globals.manifestPaths.endpoints) // HACK: HttpResourceFetcher breaks web mode when imported, so we use webpack.IgnorePlugin() @@ -224,32 +210,3 @@ export function makeEndpointsProvider() { remote: () => getEndpointsFromFetcher(remoteManifestFetcher), } } - -/** - * Wraps the `vscode.window.withProgress` functionality with functionality that also writes to the output channel. - * - * Cloud9 does not show a progress notification. - */ -function wrapWithProgressForCloud9(channel: vscode.OutputChannel): (typeof vscode.window)['withProgress'] { - const withProgress = vscode.window.withProgress.bind(vscode.window) - - return (options, task) => { - if (options.title) { - channel.appendLine(options.title) - } - - return withProgress(options, (progress, token) => { - const newProgress: typeof progress = { - ...progress, - report: (value) => { - if (value.message) { - channel.appendLine(value.message) - } - progress.report(value) - }, - } - - return task(newProgress, token) - }) - } -} diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index 0c53cc89975..e2e63614381 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -17,7 +17,6 @@ import { AwsContextCommands } from './shared/awsContextCommands' import { getIdeProperties, getExtEnvironmentDetails, - isCloud9, isSageMaker, showWelcomeMessage, } from './shared/extensionUtilities' @@ -38,6 +37,7 @@ import { activate as activateDev } from './dev/activation' import * as beta from './dev/beta' import { activate as activateApplicationComposer } from './applicationcomposer/activation' import { activate as activateRedshift } from './awsService/redshift/activation' +import { activate as activateDocumentDb } from './docdb/activation' import { activate as activateIamPolicyChecks } from './awsService/accessanalyzer/activation' import { activate as activateNotifications } from './notifications/activation' import { SchemaService } from './shared/schemas' @@ -182,19 +182,17 @@ export async function activate(context: vscode.ExtensionContext) { await activateSchemas(extContext) - if (!isCloud9()) { - if (!isSageMaker()) { - // Amazon Q/CodeWhisperer Tree setup. - learnMoreAmazonQCommand.register() - qExtensionPageCommand.register() - dismissQTree.register() - installAmazonQExtension.register() + if (!isSageMaker()) { + // Amazon Q Tree setup. + learnMoreAmazonQCommand.register() + qExtensionPageCommand.register() + dismissQTree.register() + installAmazonQExtension.register() - await handleAmazonQInstall() - } - await activateApplicationComposer(context) - await activateThreatComposerEditor(context) + await handleAmazonQInstall() } + await activateApplicationComposer(context) + await activateThreatComposerEditor(context) await activateStepFunctions(context, globals.awsContext, globals.outputChannel) @@ -202,6 +200,8 @@ export async function activate(context: vscode.ExtensionContext) { await activateAppBuilder(extContext) + await activateDocumentDb(extContext) + await activateIamPolicyChecks(extContext) context.subscriptions.push( diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index 4a21b2e9611..2b160e89bdf 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -4,6 +4,7 @@ */ import * as vscode from 'vscode' +import { Lambda } from 'aws-sdk' import { deleteLambda } from './commands/deleteLambda' import { uploadLambdaCommand } from './commands/uploadLambda' import { LambdaFunctionNode } from './explorer/lambdaFunctionNode' @@ -18,6 +19,11 @@ import { copyLambdaUrl } from './commands/copyLambdaUrl' import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode' import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { getSourceNode } from '../shared/utilities/treeNodeUtils' +import { tailLogGroup } from '../awsService/cloudWatchLogs/commands/tailLogGroup' +import { liveTailRegistry, liveTailCodeLensProvider } from '../awsService/cloudWatchLogs/activation' +import { getFunctionLogGroupName } from '../awsService/cloudWatchLogs/activation' +import { ToolkitError, isError } from '../shared' +import { LogStreamFilterResponse } from '../awsService/cloudWatchLogs/wizard/liveTailLogStreamSubmenu' /** * Activates Lambda components. @@ -72,10 +78,44 @@ export async function activate(context: ExtContext): Promise { await copyLambdaUrl(sourceNode, new DefaultLambdaClient(sourceNode.regionCode)) }), - registerSamInvokeVueCommand(context), + registerSamInvokeVueCommand(context.extensionContext), Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) => - registerSamDebugInvokeVueCommand(context, { resource: node }) - ) + registerSamDebugInvokeVueCommand(context.extensionContext, { resource: node }) + ), + + Commands.register('aws.appBuilder.tailLogs', async (node: LambdaFunctionNode | TreeNode) => { + let functionConfiguration: Lambda.FunctionConfiguration + try { + const sourceNode = getSourceNode(node) + functionConfiguration = sourceNode.configuration + const logGroupInfo = { + regionName: sourceNode.regionCode, + groupName: getFunctionLogGroupName(functionConfiguration), + } + + const source = isTreeNode(node) ? 'AppBuilder' : 'AwsExplorerLambdaNode' + // Show all log streams without having to choose + const logStreamFilterData: LogStreamFilterResponse = { type: 'all' } + await tailLogGroup( + liveTailRegistry, + source, + liveTailCodeLensProvider, + logGroupInfo, + logStreamFilterData + ) + } catch (err) { + if (isError(err as Error, 'ResourceNotFoundException', "LogGroup doesn't exist.")) { + // If we caught this error, then we know `functionConfiguration` actually has a value + throw ToolkitError.chain( + err, + `Unable to fetch logs. Log group for function '${functionConfiguration!.FunctionName}' does not exist. ` + + 'Invoking your function at least once will create the log group.' + ) + } else { + throw err + } + } + }) ) } diff --git a/packages/core/src/lambda/commands/createNewSamApp.ts b/packages/core/src/lambda/commands/createNewSamApp.ts index e60e60c31ed..8c6f5fbab19 100644 --- a/packages/core/src/lambda/commands/createNewSamApp.ts +++ b/packages/core/src/lambda/commands/createNewSamApp.ts @@ -39,12 +39,7 @@ import { isTemplateTargetProperties } from '../../shared/sam/debugger/awsSamDebu import { TemplateTargetProperties } from '../../shared/sam/debugger/awsSamDebugConfiguration' import { openLaunchJsonFile } from '../../shared/sam/debugger/commands/addSamDebugConfiguration' import { waitUntil } from '../../shared/utilities/timeoutUtils' -import { - getIdeProperties, - getDebugNewSamAppDocUrl, - isCloud9, - getLaunchConfigDocUrl, -} from '../../shared/extensionUtilities' +import { getIdeProperties, getDebugNewSamAppDocUrl, getLaunchConfigDocUrl } from '../../shared/extensionUtilities' import { checklogs } from '../../shared/localizedText' import globals from '../../shared/extensionGlobals' import { telemetry } from '../../shared/telemetry/telemetry' @@ -473,9 +468,7 @@ export async function writeToolkitReadme( .replace(/\$\{LISTOFCONFIGURATIONS\}/g, configString) .replace( /\$\{DOCURL\}/g, - isCloud9() - ? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html' - : 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html' + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html' ) await fs.writeFile(readmeLocation, readme) diff --git a/packages/core/src/lambda/commands/downloadLambda.ts b/packages/core/src/lambda/commands/downloadLambda.ts index 16128ce5701..815ff2576e9 100644 --- a/packages/core/src/lambda/commands/downloadLambda.ts +++ b/packages/core/src/lambda/commands/downloadLambda.ts @@ -14,7 +14,7 @@ import { LaunchConfiguration, getReferencedHandlerPaths } from '../../shared/deb import { makeTemporaryToolkitFolder, fileExists, tryRemoveFolder } from '../../shared/filesystemUtilities' import * as localizedText from '../../shared/localizedText' import { getLogger } from '../../shared/logger' -import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher' +import { HttpResourceFetcher } from '../../shared/resourcefetcher/node/httpResourceFetcher' import { createCodeAwsSamDebugConfig } from '../../shared/sam/debugger/awsSamDebugConfiguration' import * as pathutils from '../../shared/utilities/pathUtils' import { localize } from '../../shared/utilities/vsCodeUtils' diff --git a/packages/core/src/lambda/commands/uploadLambda.ts b/packages/core/src/lambda/commands/uploadLambda.ts index 7627d53cfab..e5a4ce34755 100644 --- a/packages/core/src/lambda/commands/uploadLambda.ts +++ b/packages/core/src/lambda/commands/uploadLambda.ts @@ -20,7 +20,7 @@ import { getSamCliContext } from '../../shared/sam/cli/samCliContext' import { SamTemplateGenerator } from '../../shared/templates/sam/samTemplateGenerator' import { addCodiconToString } from '../../shared/utilities/textUtilities' import { getLambdaDetails, listLambdaFunctions } from '../utils' -import { getIdeProperties, isCloud9 } from '../../shared/extensionUtilities' +import { getIdeProperties } from '../../shared/extensionUtilities' import { createQuickPick, DataQuickPickItem } from '../../shared/ui/pickerPrompter' import { createCommonButtons } from '../../shared/ui/buttons' import { StepEstimator, Wizard, WIZARD_BACK } from '../../shared/wizards/wizard' @@ -481,10 +481,7 @@ async function uploadZipBuffer( ) } -export async function findApplicationJsonFile( - startPath: vscode.Uri, - cloud9 = isCloud9() -): Promise { +export async function findApplicationJsonFile(startPath: vscode.Uri): Promise { if (!(await fs.exists(startPath.fsPath))) { getLogger().error( 'findApplicationJsonFile() invalid path (not accessible or does not exist): "%s"', diff --git a/packages/core/src/lambda/local/debugConfiguration.ts b/packages/core/src/lambda/local/debugConfiguration.ts index 999b36ddce2..b21599b0563 100644 --- a/packages/core/src/lambda/local/debugConfiguration.ts +++ b/packages/core/src/lambda/local/debugConfiguration.ts @@ -54,20 +54,6 @@ export interface PythonDebugConfiguration extends SamLaunchRequestArgs { readonly pathMappings: PythonPathMapping[] } -/** Alternative (Cloud9) Python debugger: ikp3db */ -export interface PythonCloud9DebugConfiguration extends SamLaunchRequestArgs { - readonly runtimeFamily: RuntimeFamily.Python - /** Passed to "sam build --manifest …" */ - readonly manifestPath: string | undefined - - // Fields expected by the Cloud9 debug adapter. - // (Cloud9 sourcefile: debugger-vscode-mainthread-adapter.ts) - readonly port: number - readonly address: string - readonly localRoot: string - readonly remoteRoot: string -} - export interface DotNetDebugConfiguration extends SamLaunchRequestArgs { readonly runtimeFamily: RuntimeFamily.DotNet processName: string diff --git a/packages/core/src/lambda/models/samTemplates.ts b/packages/core/src/lambda/models/samTemplates.ts index 963cbdaafa5..5ec112a7dc4 100644 --- a/packages/core/src/lambda/models/samTemplates.ts +++ b/packages/core/src/lambda/models/samTemplates.ts @@ -12,10 +12,26 @@ import { supportsEventBridgeTemplates } from '../../../src/eventSchemas/models/s import { RuntimePackageType } from './samLambdaRuntime' import { getIdeProperties } from '../../shared/extensionUtilities' -export let helloWorldTemplate = 'helloWorldUninitialized' -export let eventBridgeHelloWorldTemplate = 'eventBridgeHelloWorldUninitialized' -export let eventBridgeStarterAppTemplate = 'eventBridgeStarterAppUnintialized' -export let stepFunctionsSampleApp = 'stepFunctionsSampleAppUnintialized' +export const helloWorldTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM Hello World', + getIdeProperties().company +) +export const eventBridgeHelloWorldTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM EventBridge Hello World', + getIdeProperties().company +) +export const eventBridgeStarterAppTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM EventBridge App from Scratch', + getIdeProperties().company +) +export const stepFunctionsSampleApp = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} Step Functions Sample App', + getIdeProperties().company +) export const typeScriptBackendTemplate = 'App Backend using TypeScript' export const repromptUserForTemplate = 'REQUIRES_AWS_CREDENTIALS_REPROMPT_USER_FOR_TEMPLATE' @@ -23,33 +39,6 @@ export const cliVersionStepFunctionsTemplate = '0.52.0' export type SamTemplate = string -/** - * Lazy load strings for SAM template quick picks - * Need to be lazyloaded as `getIdeProperties` requires IDE activation for Cloud9 - */ -export function lazyLoadSamTemplateStrings(): void { - helloWorldTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM Hello World', - getIdeProperties().company - ) - eventBridgeHelloWorldTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM EventBridge Hello World', - getIdeProperties().company - ) - eventBridgeStarterAppTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM EventBridge App from Scratch', - getIdeProperties().company - ) - stepFunctionsSampleApp = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} Step Functions Sample App', - getIdeProperties().company - ) -} - export function getSamTemplateWizardOption( runtime: Runtime, packageType: RuntimePackageType, diff --git a/packages/core/src/lambda/utils.ts b/packages/core/src/lambda/utils.ts index 63b4325da55..7fa56bc33e9 100644 --- a/packages/core/src/lambda/utils.ts +++ b/packages/core/src/lambda/utils.ts @@ -13,8 +13,6 @@ import { CloudFormationClient } from '../shared/clients/cloudFormationClient' import { LambdaClient } from '../shared/clients/lambdaClient' import { getFamily, getNodeMajorVersion, RuntimeFamily } from './models/samLambdaRuntime' import { getLogger } from '../shared/logger' -import { ResourceFetcher } from '../shared/resourcefetcher/resourcefetcher' -import { CompositeResourceFetcher } from '../shared/resourcefetcher/compositeResourceFetcher' import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher' import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher' import { sampleRequestManifestPath } from './constants' @@ -99,7 +97,7 @@ interface SampleRequestManifest { export async function getSampleLambdaPayloads(): Promise { const logger = getLogger() - const sampleInput = await makeSampleRequestManifestResourceFetcher().get() + const sampleInput = await getSampleRequestManifest() if (!sampleInput) { throw new Error('Unable to retrieve Sample Request manifest') @@ -120,9 +118,11 @@ export async function getSampleLambdaPayloads(): Promise { return inputs } -function makeSampleRequestManifestResourceFetcher(): ResourceFetcher { - return new CompositeResourceFetcher( - new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }), - new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests) - ) +async function getSampleRequestManifest(): Promise { + const httpResp = await new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }).get() + if (!httpResp) { + const fileResp = new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests) + return fileResp.get() + } + return httpResp.text() } diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts index 643ea4631e2..ba624536b0f 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts +++ b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts @@ -7,8 +7,6 @@ import * as path from 'path' import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { LaunchConfiguration } from '../../../shared/debug/launchConfiguration' - -import { ExtContext } from '../../../shared/extensions' import { getLogger } from '../../../shared/logger' import { HttpResourceFetcher } from '../../../shared/resourcefetcher/httpResourceFetcher' import { @@ -28,8 +26,6 @@ import { tryGetAbsolutePath } from '../../../shared/utilities/workspaceUtils' import * as CloudFormation from '../../../shared/cloudformation/cloudformation' import { openLaunchJsonFile } from '../../../shared/sam/debugger/commands/addSamDebugConfiguration' import { getSampleLambdaPayloads } from '../../utils' -import { isCloud9 } from '../../../shared/extensionUtilities' -import { SamDebugConfigProvider } from '../../../shared/sam/debugger/awsSamDebugger' import { samLambdaCreatableRuntimes } from '../../models/samLambdaRuntime' import globals from '../../../shared/extensionGlobals' import { VueWebview } from '../../../webviews/main' @@ -78,7 +74,6 @@ export class SamInvokeWebview extends VueWebview { public readonly id = 'createLambda' public constructor( - private readonly extContext: ExtContext, // TODO(sijaden): get rid of `ExtContext` private readonly config?: AwsSamDebuggerConfiguration, private readonly data?: ResourceData ) { @@ -170,7 +165,8 @@ export class SamInvokeWebview extends VueWebview { return } const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}` - const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' + const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get() + const sample = (await resp?.text()) ?? '' return sample } catch (err) { @@ -357,17 +353,8 @@ export class SamInvokeWebview extends VueWebview { const targetUri = await this.getUriFromLaunchConfig(finalConfig) const folder = targetUri ? vscode.workspace.getWorkspaceFolder(targetUri) : undefined - // Cloud9 currently can't resolve the `aws-sam` debug config provider. - // Directly invoke the config instead. - // NOTE: This bypasses the `${workspaceFolder}` resolution, but shouldn't naturally occur in Cloud9 - // (Cloud9 also doesn't currently have variable resolution support anyways) - if (isCloud9()) { - const provider = new SamDebugConfigProvider(this.extContext) - await provider.resolveDebugConfiguration(folder, finalConfig, undefined, source) - } else { - // startDebugging on VS Code goes through the whole resolution chain - await vscode.debug.startDebugging(folder, finalConfig) - } + // startDebugging on VS Code goes through the whole resolution chain + await vscode.debug.startDebugging(folder, finalConfig) } public async getLaunchConfigQuickPickItems( launchConfig: LaunchConfiguration, @@ -426,9 +413,9 @@ export class SamInvokeWebview extends VueWebview { const WebviewPanel = VueWebview.compilePanel(SamInvokeWebview) -export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposable { +export function registerSamInvokeVueCommand(context: vscode.ExtensionContext): vscode.Disposable { return Commands.register('aws.launchConfigForm', async (launchConfig?: AwsSamDebuggerConfiguration) => { - const webview = new WebviewPanel(context.extensionContext, context, launchConfig) + const webview = new WebviewPanel(context, launchConfig) await telemetry.sam_openConfigUi.run(async (span) => { await webview.show({ title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'), @@ -439,11 +426,14 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa }) } -export async function registerSamDebugInvokeVueCommand(context: ExtContext, params: { resource: ResourceNode }) { +export async function registerSamDebugInvokeVueCommand( + context: vscode.ExtensionContext, + params: { resource: ResourceNode } +) { const launchConfig: AwsSamDebuggerConfiguration | undefined = undefined const resource = params?.resource.resource const source = 'AppBuilderLocalInvoke' - const webview = new WebviewPanel(context.extensionContext, context, launchConfig, { + const webview = new WebviewPanel(context, launchConfig, { logicalId: resource.resource.Id ?? '', region: resource.region ?? '', location: resource.location.fsPath, diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index 501304c1a94..38b3700719c 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -219,7 +219,8 @@ export class RemoteInvokeWebview extends VueWebview { return } const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}` - const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' + const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get() + const sample = (await resp?.text()) ?? '' return sample } catch (err) { diff --git a/packages/core/src/login/webview/commonAuthViewProvider.ts b/packages/core/src/login/webview/commonAuthViewProvider.ts index d5f94748e07..8f641acdb8d 100644 --- a/packages/core/src/login/webview/commonAuthViewProvider.ts +++ b/packages/core/src/login/webview/commonAuthViewProvider.ts @@ -11,7 +11,7 @@ "type": "webview", "id": "aws.AmazonCommonAuth", "name": "%AWS.amazonq.login%", -"when": "!isCloud9 && !aws.isSageMaker && !aws.amazonq.showView" +"when": "!aws.isSageMaker && !aws.amazonq.showView" }, * 2. Assign when clause context to this view. Manage the state of when clause context. diff --git a/packages/core/src/login/webview/vue/backend.ts b/packages/core/src/login/webview/vue/backend.ts index 0c1cbdaebc7..ed467175334 100644 --- a/packages/core/src/login/webview/vue/backend.ts +++ b/packages/core/src/login/webview/vue/backend.ts @@ -31,6 +31,7 @@ import { AuthEnabledFeatures, AuthError, AuthFlowState, AuthUiClick, userCancell import { DevSettings } from '../../../shared/settings' import { AuthSSOServer } from '../../../auth/sso/server' import { getLogger } from '../../../shared/logger/logger' +import { isValidUrl } from '../../../shared/utilities/uriUtils' export abstract class CommonAuthWebview extends VueWebview { private readonly className = 'CommonAuthWebview' @@ -276,4 +277,8 @@ export abstract class CommonAuthWebview extends VueWebview { cancelAuthFlow() { AuthSSOServer.lastInstance?.cancelCurrentFlow() } + + validateUrl(url: string) { + return isValidUrl(url) + } } diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index f15848a9069..4c9f65a2f6a 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -193,6 +193,7 @@ @keydown.enter="handleContinueClick()" />

{{ startUrlError }}

+

{{ startUrlWarning }}

Region
AWS Region that hosts identity directory