diff --git a/src/cli/__tests__/errors.test.ts b/src/cli/__tests__/errors.test.ts index 663cd3508..9be22167a 100644 --- a/src/cli/__tests__/errors.test.ts +++ b/src/cli/__tests__/errors.test.ts @@ -1,5 +1,6 @@ import { AgentAlreadyExistsError, toError } from '../../lib'; import { + formatError, getErrorMessage, isChangesetInProgressError, isExpiredTokenError, @@ -39,6 +40,18 @@ describe('errors', () => { }); }); + describe('formatError', () => { + it('surfaces the cause chain that getErrorMessage drops (CDK synth stderr)', () => { + const causeText = 'AgentCore CDK synthesis failed: memory strategy "foo" is invalid'; + const err = new Error('CDK synth failed: node dist/bin/cdk.js: Subprocess exited with error 1', { + cause: new Error(causeText), + }); + + expect(getErrorMessage(err)).not.toContain(causeText); + expect(formatError(err)).toContain(causeText); + }); + }); + describe('isExpiredTokenError', () => { // Test ALL error codes in EXPIRED_TOKEN_ERROR_CODES via error.name const allErrorCodes = [ diff --git a/src/cli/commands/deploy/command.tsx b/src/cli/commands/deploy/command.tsx index 0af261125..1a37c3366 100644 --- a/src/cli/commands/deploy/command.tsx +++ b/src/cli/commands/deploy/command.tsx @@ -1,6 +1,6 @@ import { ConfigIO, serializeResult } from '../../../lib'; import { COMMAND_DESCRIPTIONS } from '../../constants'; -import { getErrorMessage } from '../../errors'; +import { formatError, getErrorMessage } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { renderTUI } from '../../tui'; import { requireProject, requireTTY } from '../../tui/guards'; @@ -57,7 +57,7 @@ async function handleDeployCLI(options: DeployOptions): Promise { if (options.json) { console.log(JSON.stringify(serializeResult(deployResult))); } else { - console.error(deployResult.error.message); + console.error(formatError(deployResult.error)); if (deployResult.logPath) { console.error(`Log: ${deployResult.logPath}`); } diff --git a/src/cli/commands/import/command.ts b/src/cli/commands/import/command.ts index 965fc8767..b9223bb8f 100644 --- a/src/cli/commands/import/command.ts +++ b/src/cli/commands/import/command.ts @@ -1,5 +1,6 @@ import { ValidationError } from '../../../lib'; import { ANSI } from '../../constants'; +import { formatError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { handleImport } from './actions'; import { registerImportEvaluator } from './import-evaluator'; @@ -144,7 +145,7 @@ export const registerImport = (program: Command) => { console.log(`Log: ${result.logPath}`); } } else { - console.error(`\n${red}[error]${reset} Import failed: ${result.error.message}`); + console.error(`\n${red}[error]${reset} Import failed: ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-evaluator.ts b/src/cli/commands/import/import-evaluator.ts index 71a60d833..863ed2a45 100644 --- a/src/cli/commands/import/import-evaluator.ts +++ b/src/cli/commands/import/import-evaluator.ts @@ -9,6 +9,7 @@ import { listAllOnlineEvaluationConfigs, } from '../../aws/agentcore-control'; import { ANSI } from '../../constants'; +import { formatError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { failResult, parseAndValidateArn } from './import-utils'; import { executeResourceImport } from './resource-import'; @@ -164,7 +165,7 @@ export function registerImportEvaluator(importCmd: Command): void { console.log(` ID: ${result.resourceId}`); console.log(''); } else { - console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-gateway.ts b/src/cli/commands/import/import-gateway.ts index 163a6ede2..736b7b61f 100644 --- a/src/cli/commands/import/import-gateway.ts +++ b/src/cli/commands/import/import-gateway.ts @@ -19,7 +19,7 @@ import { listAllGateways, } from '../../aws/agentcore-control'; import { ANSI } from '../../constants'; -import { isAccessDeniedError } from '../../errors'; +import { formatError, isAccessDeniedError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { executeCdkImportPipeline } from './import-pipeline'; import { @@ -746,7 +746,7 @@ export function registerImportGateway(importCmd: Command): void { console.log(` agentcore fetch access ${ANSI.dim}Get gateway URL and token${ANSI.reset}`); console.log(''); } else { - console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-memory.ts b/src/cli/commands/import/import-memory.ts index 90a799633..d559f2d76 100644 --- a/src/cli/commands/import/import-memory.ts +++ b/src/cli/commands/import/import-memory.ts @@ -3,6 +3,7 @@ import { IndexedKeyTypeSchema } from '../../../schema'; import type { MemoryDetail, MemorySummary } from '../../aws/agentcore-control'; import { getMemoryDetail, listAllMemories } from '../../aws/agentcore-control'; import { ANSI } from '../../constants'; +import { formatError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { parseAndValidateArn } from './import-utils'; import { executeResourceImport } from './resource-import'; @@ -142,7 +143,7 @@ export function registerImportMemory(importCmd: Command): void { console.log(` ID: ${result.resourceId}`); console.log(''); } else { - console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-online-eval.ts b/src/cli/commands/import/import-online-eval.ts index 94426ac14..febb93d7e 100644 --- a/src/cli/commands/import/import-online-eval.ts +++ b/src/cli/commands/import/import-online-eval.ts @@ -9,6 +9,7 @@ import { } from '../../aws/agentcore-control'; import { arnPrefix } from '../../aws/partition'; import { ANSI } from '../../constants'; +import { formatError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { failResult, findResourceInDeployedState, parseAndValidateArn } from './import-utils'; import { executeResourceImport } from './resource-import'; @@ -219,7 +220,7 @@ export function registerImportOnlineEval(importCmd: Command): void { console.log(` ID: ${result.resourceId}`); console.log(''); } else { - console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-runtime.ts b/src/cli/commands/import/import-runtime.ts index 58aa53fad..460b9b7dd 100644 --- a/src/cli/commands/import/import-runtime.ts +++ b/src/cli/commands/import/import-runtime.ts @@ -2,6 +2,7 @@ import type { AgentEnvSpec } from '../../../schema'; import type { AgentRuntimeDetail, AgentRuntimeSummary } from '../../aws/agentcore-control'; import { getAgentRuntimeDetail, listAllAgentRuntimes } from '../../aws/agentcore-control'; import { ANSI } from '../../constants'; +import { formatError } from '../../errors'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; import { copyAgentSource, failResult, parseAndValidateArn } from './import-utils'; import { executeResourceImport } from './resource-import'; @@ -227,7 +228,7 @@ export function registerImportRuntime(importCmd: Command): void { console.log(` agentcore invoke ${ANSI.dim}Test your agent${ANSI.reset}`); console.log(''); } else { - console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${formatError(result.error)}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/errors.ts b/src/cli/errors.ts index ccca6fa16..e4adcea34 100644 --- a/src/cli/errors.ts +++ b/src/cli/errors.ts @@ -1,11 +1,38 @@ /** * Converts an unknown error to a string message. * Handles Error instances and other thrown values consistently. + * + * Note: this intentionally returns only `err.message` and does NOT walk the + * cause chain. Use `formatError` when displaying an error to the user so the + * underlying cause (e.g. CDK synth child stderr) is surfaced. */ export function getErrorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } +/** + * Format an error for user display. Shows the message and the full cause chain, but NOT the raw JS + * stack trace — dumping `err.stack` (minified dist frames) to users is noise for the common case + * (config/validation errors). Set AGENTCORE_DEBUG=1 to include the stack trace for debugging. + * + * Unlike `getErrorMessage`, this recurses into `err.cause`, so actionable detail attached by lower + * layers (e.g. the CDK toolkit wrapper preserving the synth child's stderr on `err.cause`) reaches + * the user instead of being dropped behind a generic top-level message. + */ +export function formatError(err: unknown): string { + if (err instanceof Error) { + const lines = [err.message]; + if (err.stack && process.env.AGENTCORE_DEBUG) { + lines.push('', 'Stack trace:', err.stack); + } + if (err.cause) { + lines.push('', 'Caused by:', formatError(err.cause)); + } + return lines.join('\n'); + } + return String(err); +} + /** * Checks if an error is an AWS access denied error. * Returns true for AccessDeniedException or AccessDenied error codes. diff --git a/src/cli/operations/deploy/preflight.ts b/src/cli/operations/deploy/preflight.ts index a06321578..21b04b667 100644 --- a/src/cli/operations/deploy/preflight.ts +++ b/src/cli/operations/deploy/preflight.ts @@ -5,6 +5,7 @@ import { validateAwsCredentials } from '../../aws/account'; import { LocalCdkProject } from '../../cdk/local-cdk-project'; import { CdkToolkitWrapper, createCdkToolkitWrapper, silentIoHost } from '../../cdk/toolkit-lib'; import { checkBootstrapStatus, checkStacksStatus, formatCdkEnvironment } from '../../cloudformation'; +import { formatError } from '../../errors'; import { cleanupStaleLockFiles } from '../../tui/utils'; import type { IIoHost } from '@aws-cdk/toolkit-lib'; import { existsSync, readFileSync } from 'node:fs'; @@ -39,24 +40,10 @@ export interface StackStatusCheckResult { message?: string; } -/** - * Format an error for user display. Shows the message and the cause chain, but NOT the raw JS stack - * trace — dumping `err.stack` (minified dist/cli/index.mjs frames) to users is noise for the common - * case (config/validation errors). Set AGENTCORE_DEBUG=1 to include the stack trace for debugging. - */ -export function formatError(err: unknown): string { - if (err instanceof Error) { - const lines = [err.message]; - if (err.stack && process.env.AGENTCORE_DEBUG) { - lines.push('', 'Stack trace:', err.stack); - } - if (err.cause) { - lines.push('', 'Caused by:', formatError(err.cause)); - } - return lines.join('\n'); - } - return String(err); -} +// Re-exported from src/cli/errors.ts so existing importers (TUI hooks, operations/deploy/index.ts, +// preflight tests) keep their import path. Lives in errors.ts so the thin command/display layers can +// reuse it without pulling in this heavy deploy module. +export { formatError }; /** * Validates the CDK project and loads configuration.