From f95fe4633c7862f202cc69f6c58b8e7f8957d126 Mon Sep 17 00:00:00 2001 From: Abhimanyu Siwach Date: Wed, 25 Feb 2026 13:43:57 -0800 Subject: [PATCH] feat: add version-aware AWS CLI guidance to credential error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credential error messages now include contextual AWS CLI guidance instead of hardcoded "Run: aws login" strings. When the CLI is missing or below v2.32.0, users see install/upgrade instructions inline — the overall message structure stays the same. Also updates stale "aws sso login" reference to "aws login" in pre-deploy identity flow. --- src/cli/aws/account.ts | 10 ++- src/cli/external-requirements/checks.ts | 71 ++++++++++++++++++- src/cli/external-requirements/index.ts | 12 +++- src/cli/external-requirements/versions.ts | 3 + .../operations/deploy/pre-deploy-identity.ts | 3 +- 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/cli/aws/account.ts b/src/cli/aws/account.ts index 6b2c2343..705d25ce 100644 --- a/src/cli/aws/account.ts +++ b/src/cli/aws/account.ts @@ -1,3 +1,4 @@ +import { getAwsLoginGuidance } from '../external-requirements/checks'; import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; import { fromEnv, fromNodeProviderChain } from '@aws-sdk/credential-providers'; import type { AwsCredentialIdentityProvider } from '@smithy/types'; @@ -46,16 +47,18 @@ export async function detectAccount(): Promise { const code = (err as { name?: string })?.name ?? (err as { Code?: string })?.Code; if (code === 'ExpiredTokenException' || code === 'ExpiredToken') { + const guidance = await getAwsLoginGuidance(); throw new AwsCredentialsError( 'AWS credentials expired.', - 'AWS credentials expired.\n\nTo fix this:\n Run: aws login' + `AWS credentials expired.\n\nTo fix this:\n ${guidance}` ); } if (code === 'InvalidClientTokenId' || code === 'SignatureDoesNotMatch') { + const guidance = await getAwsLoginGuidance(); throw new AwsCredentialsError( 'AWS credentials are invalid.', - 'AWS credentials are invalid.\n\nTo fix this:\n 1. Check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY\n 2. Or run: aws login' + `AWS credentials are invalid.\n\nTo fix this:\n 1. Check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY\n 2. Or ${guidance}` ); } @@ -77,11 +80,12 @@ export async function detectAccount(): Promise { export async function validateAwsCredentials(): Promise { const account = await detectAccount(); if (!account) { + const guidance = await getAwsLoginGuidance(); throw new AwsCredentialsError( 'No AWS credentials configured.', 'No AWS credentials configured.\n\n' + 'To fix this:\n' + - ' 1. Run: aws login\n' + + ` 1. ${guidance}\n` + ' 2. Or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables' ); } diff --git a/src/cli/external-requirements/checks.ts b/src/cli/external-requirements/checks.ts index 59632c64..33226471 100644 --- a/src/cli/external-requirements/checks.ts +++ b/src/cli/external-requirements/checks.ts @@ -4,7 +4,7 @@ import { checkSubprocess, isWindows, runSubprocessCapture } from '../../lib'; import type { AgentCoreProjectSpec, TargetLanguage } from '../../schema'; import { detectContainerRuntime } from './detect'; -import { NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions'; +import { AWS_CLI_MIN_VERSION, NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions'; /** * Result of a version check. @@ -70,6 +70,73 @@ export async function checkUvVersion(): Promise { return { satisfied: true, current, required: 'any', binary: 'uv' }; } +const AWS_CLI_INSTALL_URL = 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html'; + +/** + * Extract version from `aws --version` output. + * Expected format: "aws-cli/2.32.0 Python/3.11.6 Darwin/23.3.0 ..." + */ +function parseAwsCliVersion(output: string): string | null { + const match = /aws-cli\/(\d+\.\d+\.\d+)/.exec(output.trim()); + return match?.[1] ?? null; +} + +/** + * Check that AWS CLI meets minimum version requirement for `aws login`. + */ +export async function checkAwsCliVersion(): Promise { + const required = formatSemVer(AWS_CLI_MIN_VERSION); + + const result = await runSubprocessCapture('aws', ['--version']); + if (result.code !== 0) { + return { satisfied: false, current: null, required, binary: 'aws' }; + } + + const versionStr = parseAwsCliVersion(result.stdout); + if (!versionStr) { + return { satisfied: false, current: null, required, binary: 'aws' }; + } + + const current = parseSemVer(versionStr); + if (!current) { + return { satisfied: false, current: versionStr, required, binary: 'aws' }; + } + + return { + satisfied: semVerGte(current, AWS_CLI_MIN_VERSION), + current: versionStr, + required, + binary: 'aws', + }; +} + +/** Cached result for getAwsLoginGuidance */ +let _awsLoginGuidance: string | null = null; + +/** + * Get version-aware guidance for authenticating with AWS. + * Checks if AWS CLI is installed and whether it supports `aws login`. + * Result is cached for the lifetime of the process. + */ +export async function getAwsLoginGuidance(): Promise { + if (_awsLoginGuidance) return _awsLoginGuidance; + + const check = await checkAwsCliVersion(); + + if (check.current === null) { + // AWS CLI not installed + _awsLoginGuidance = `Install AWS CLI (v${formatSemVer(AWS_CLI_MIN_VERSION)}+) from ${AWS_CLI_INSTALL_URL} and run: aws login`; + } else if (!check.satisfied) { + // AWS CLI installed but too old for `aws login` + _awsLoginGuidance = `Update AWS CLI from v${check.current} to v${formatSemVer(AWS_CLI_MIN_VERSION)}+ (${AWS_CLI_INSTALL_URL}) and run: aws login`; + } else { + // AWS CLI is new enough + _awsLoginGuidance = 'Run: aws login'; + } + + return _awsLoginGuidance; +} + /** * Format a version check failure as a user-friendly error message. */ @@ -234,7 +301,7 @@ export async function checkCreateDependencies( }); if (!awsAvailable) { warnings.push( - "'aws' CLI not found. Required for 'aws sso login' and profile configuration. Install from https://aws.amazon.com/cli/" + `'aws' CLI not found. Required for 'aws login'. Install v${formatSemVer(AWS_CLI_MIN_VERSION)}+ from ${AWS_CLI_INSTALL_URL}` ); } diff --git a/src/cli/external-requirements/index.ts b/src/cli/external-requirements/index.ts index 6751aafa..76b77ebc 100644 --- a/src/cli/external-requirements/index.ts +++ b/src/cli/external-requirements/index.ts @@ -1,8 +1,18 @@ -export { parseSemVer, compareSemVer, semVerGte, formatSemVer, NODE_MIN_VERSION, type SemVer } from './versions'; +export { + parseSemVer, + compareSemVer, + semVerGte, + formatSemVer, + NODE_MIN_VERSION, + AWS_CLI_MIN_VERSION, + type SemVer, +} from './versions'; export { checkNodeVersion, checkUvVersion, + checkAwsCliVersion, + getAwsLoginGuidance, formatVersionError, requiresUv, requiresContainerRuntime, diff --git a/src/cli/external-requirements/versions.ts b/src/cli/external-requirements/versions.ts index 4cdbfdcc..02050491 100644 --- a/src/cli/external-requirements/versions.ts +++ b/src/cli/external-requirements/versions.ts @@ -59,3 +59,6 @@ export function formatSemVer(v: SemVer): string { /** Minimum Node.js version required for CDK synth (ES2022 target) */ export const NODE_MIN_VERSION: SemVer = { major: 18, minor: 0, patch: 0 }; + +/** Minimum AWS CLI version required for `aws login` */ +export const AWS_CLI_MIN_VERSION: SemVer = { major: 2, minor: 32, patch: 0 }; diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index f33ee595..a24d1c09 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -2,6 +2,7 @@ import { SecureCredentials, readEnvFile } from '../../../lib'; import type { AgentCoreProjectSpec, Credential } from '../../../schema'; import { getCredentialProvider } from '../../aws'; import { isNoCredentialsError } from '../../errors'; +import { getAwsLoginGuidance } from '../../external-requirements/checks'; import { apiKeyProviderExists, createApiKeyProvider, setTokenVaultKmsKey, updateApiKeyProvider } from '../identity'; import { computeDefaultCredentialEnvVarName } from '../identity/create-identity'; import { BedrockAgentCoreControlClient, GetTokenVaultCommand } from '@aws-sdk/client-bedrock-agentcore-control'; @@ -171,7 +172,7 @@ async function setupApiKeyCredentialProvider( // Provide clearer error message for AWS credentials issues let errorMessage: string; if (isNoCredentialsError(error)) { - errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.'; + errorMessage = `AWS credentials not found. ${await getAwsLoginGuidance()}`; } else { errorMessage = error instanceof Error ? error.message : String(error); }