diff --git a/src/cli/aws/__tests__/transaction-search.test.ts b/src/cli/aws/__tests__/transaction-search.test.ts index 67ccd0ac6..33965d071 100644 --- a/src/cli/aws/__tests__/transaction-search.test.ts +++ b/src/cli/aws/__tests__/transaction-search.test.ts @@ -163,7 +163,8 @@ describe('enableTransactionSearch', () => { const result = await enableTransactionSearch('us-east-1', '123456789012'); expect(result.success).toBe(false); - expect(result.error).toContain('Insufficient permissions to enable Application Signals'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Insufficient permissions to enable Application Signals'); }); it('returns error when Application Signals fails with generic error', async () => { @@ -172,7 +173,8 @@ describe('enableTransactionSearch', () => { const result = await enableTransactionSearch('us-east-1', '123456789012'); expect(result.success).toBe(false); - expect(result.error).toContain('Failed to enable Application Signals'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Failed to enable Application Signals'); }); it('returns error when CloudWatch Logs policy fails with AccessDenied', async () => { @@ -184,7 +186,8 @@ describe('enableTransactionSearch', () => { const result = await enableTransactionSearch('us-east-1', '123456789012'); expect(result.success).toBe(false); - expect(result.error).toContain('Insufficient permissions to configure CloudWatch Logs policy'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Insufficient permissions to configure CloudWatch Logs policy'); }); it('returns error when trace destination fails', async () => { @@ -195,7 +198,8 @@ describe('enableTransactionSearch', () => { const result = await enableTransactionSearch('us-east-1', '123456789012'); expect(result.success).toBe(false); - expect(result.error).toContain('Failed to configure trace destination'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Failed to configure trace destination'); }); it('returns error when indexing rule update fails', async () => { @@ -215,7 +219,8 @@ describe('enableTransactionSearch', () => { const result = await enableTransactionSearch('us-east-1', '123456789012'); expect(result.success).toBe(false); - expect(result.error).toContain('Failed to configure indexing rules'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Failed to configure indexing rules'); }); it('does not proceed to later steps when an earlier step fails', async () => { diff --git a/src/cli/aws/transaction-search.ts b/src/cli/aws/transaction-search.ts index 3a3b53fa6..e60e67206 100644 --- a/src/cli/aws/transaction-search.ts +++ b/src/cli/aws/transaction-search.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../lib/types'; import { getErrorMessage, isAccessDeniedError } from '../errors'; import { getCredentialProvider } from './account'; import { arnPrefix } from './partition'; @@ -14,10 +15,7 @@ import { XRayClient, } from '@aws-sdk/client-xray'; -export interface TransactionSearchEnableResult { - success: boolean; - error?: string; -} +export type TransactionSearchEnableResult = Result; const RESOURCE_POLICY_NAME = 'TransactionSearchXRayAccess'; @@ -44,9 +42,9 @@ export async function enableTransactionSearch( } catch (err: unknown) { const message = getErrorMessage(err); if (isAccessDeniedError(err)) { - return { success: false, error: `Insufficient permissions to enable Application Signals: ${message}` }; + return { success: false, error: new Error(`Insufficient permissions to enable Application Signals: ${message}`) }; } - return { success: false, error: `Failed to enable Application Signals: ${message}` }; + return { success: false, error: new Error(`Failed to enable Application Signals: ${message}`) }; } // Step 2: Create CloudWatch Logs resource policy for X-Ray (if needed) @@ -80,9 +78,12 @@ export async function enableTransactionSearch( } catch (err: unknown) { const message = getErrorMessage(err); if (isAccessDeniedError(err)) { - return { success: false, error: `Insufficient permissions to configure CloudWatch Logs policy: ${message}` }; + return { + success: false, + error: new Error(`Insufficient permissions to configure CloudWatch Logs policy: ${message}`), + }; } - return { success: false, error: `Failed to configure CloudWatch Logs policy: ${message}` }; + return { success: false, error: new Error(`Failed to configure CloudWatch Logs policy: ${message}`) }; } const xrayClient = new XRayClient({ region, credentials }); @@ -96,9 +97,12 @@ export async function enableTransactionSearch( } catch (err: unknown) { const message = getErrorMessage(err); if (isAccessDeniedError(err)) { - return { success: false, error: `Insufficient permissions to configure trace destination: ${message}` }; + return { + success: false, + error: new Error(`Insufficient permissions to configure trace destination: ${message}`), + }; } - return { success: false, error: `Failed to configure trace destination: ${message}` }; + return { success: false, error: new Error(`Failed to configure trace destination: ${message}`) }; } // Step 4: Set indexing to 100% on the built-in Default rule (always exists, idempotent) @@ -112,9 +116,9 @@ export async function enableTransactionSearch( } catch (err: unknown) { const message = getErrorMessage(err); if (isAccessDeniedError(err)) { - return { success: false, error: `Insufficient permissions to configure indexing rules: ${message}` }; + return { success: false, error: new Error(`Insufficient permissions to configure indexing rules: ${message}`) }; } - return { success: false, error: `Failed to configure indexing rules: ${message}` }; + return { success: false, error: new Error(`Failed to configure indexing rules: ${message}`) }; } return { success: true }; diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index a066b1cc3..089abcfe1 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import type { GatewayAuthorizerType, ModelProvider, @@ -41,12 +42,7 @@ export interface AddAgentOptions extends VpcOptions { json?: boolean; } -export interface AddAgentResult { - success: boolean; - agentName?: string; - agentPath?: string; - error?: string; -} +export type AddAgentResult = Result<{ agentName?: string; agentPath?: string }>; // Gateway types export interface AddGatewayOptions { @@ -68,11 +64,7 @@ export interface AddGatewayOptions { json?: boolean; } -export interface AddGatewayResult { - success: boolean; - gatewayName?: string; - error?: string; -} +export type AddGatewayResult = Result<{ gatewayName?: string }>; // Gateway Target types export interface AddGatewayTargetOptions { @@ -100,12 +92,7 @@ export interface AddGatewayTargetOptions { json?: boolean; } -export interface AddGatewayTargetResult { - success: boolean; - toolName?: string; - sourcePath?: string; - error?: string; -} +export type AddGatewayTargetResult = Result<{ toolName?: string; sourcePath?: string }>; // Memory types (v2: no owner/user concept) export interface AddMemoryOptions { @@ -119,11 +106,7 @@ export interface AddMemoryOptions { json?: boolean; } -export interface AddMemoryResult { - success: boolean; - memoryName?: string; - error?: string; -} +export type AddMemoryResult = Result<{ memoryName?: string }>; // Credential types (v2: credential, no owner/user concept) export interface AddCredentialOptions { @@ -140,8 +123,4 @@ export interface AddCredentialOptions { /** @deprecated Use AddCredentialOptions */ export type AddIdentityOptions = AddCredentialOptions; -export interface AddCredentialResult { - success: boolean; - credentialName?: string; - error?: string; -} +export type AddCredentialResult = Result<{ credentialName?: string }>; diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index a00397f38..02bb1dd2e 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -57,7 +57,7 @@ export async function createProject(options: CreateProjectOptions): Promise 0 ? depWarnings : undefined, }; } catch (err) { - return { success: false, error: getErrorMessage(err), warnings: depWarnings }; + return { success: false, error: new Error(getErrorMessage(err)), warnings: depWarnings }; } } @@ -174,7 +174,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P // Fail on errors if (!depCheck.passed) { - return { success: false, error: depCheck.errors.join('\n'), warnings: depWarnings }; + return { success: false, error: new Error(depCheck.errors.join('\n')), warnings: depWarnings }; } // First create the base project (skip dependency check since we already did it) @@ -217,7 +217,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P warnings: depWarnings.length > 0 ? depWarnings : undefined, }; } catch (err) { - return { success: false, error: getErrorMessage(err), warnings: depWarnings }; + return { success: false, error: new Error(getErrorMessage(err)), warnings: depWarnings }; } } @@ -310,7 +310,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P warnings: depWarnings.length > 0 ? depWarnings : undefined, }; } catch (err) { - return { success: false, error: getErrorMessage(err), warnings: depWarnings }; + return { success: false, error: new Error(getErrorMessage(err)), warnings: depWarnings }; } } diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index 42c8c7403..7b5cddc89 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -94,7 +94,7 @@ async function handleCreateCLI(options: CreateOptions): Promise { const result = getDryRunInfo({ name: name!, projectName, cwd, language: options.language }); if (options.json) { console.log(JSON.stringify(result)); - } else { + } else if (result.success) { console.log('Dry run - would create:'); for (const path of result.wouldCreate ?? []) { console.log(` ${path}`); @@ -158,7 +158,7 @@ async function handleCreateCLI(options: CreateOptions): Promise { }); if (options.json) { - console.log(JSON.stringify(result)); + console.log(JSON.stringify(result.success ? result : { ...result, error: result.error.message })); } else if (result.success) { printCreateSummary(projectName!, result.agentName, options.language, options.framework); if (options.skipInstall) { @@ -167,7 +167,7 @@ async function handleCreateCLI(options: CreateOptions): Promise { ); } } else { - console.error(result.error); + console.error(result.error.message); } process.exit(result.success ? 0 : 1); diff --git a/src/cli/commands/create/types.ts b/src/cli/commands/create/types.ts index f762a36eb..4216edf83 100644 --- a/src/cli/commands/create/types.ts +++ b/src/cli/commands/create/types.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import type { VpcOptions } from '../shared/vpc-utils'; export interface CreateOptions extends VpcOptions { @@ -28,12 +29,10 @@ export interface CreateOptions extends VpcOptions { json?: boolean; } -export interface CreateResult { - success: boolean; +export type CreateResult = Result<{ projectPath?: string; agentName?: string; - error?: string; dryRun?: boolean; wouldCreate?: string[]; warnings?: string[]; -} +}> & { warnings?: string[] }; diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index acae6fb03..5573bc0a6 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -85,7 +85,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise { } if (options.json) { - console.log(JSON.stringify(result)); + console.log(JSON.stringify(result.success ? result : { ...result, error: result.error.message })); } else if (result.success) { if (options.diff) { console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`); @@ -125,7 +125,7 @@ async function handleDeployCLI(options: DeployOptions): Promise { console.log(`\nLog: ${result.logPath}`); } } else { - console.error(result.error); + console.error(result.error.message); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/deploy/types.ts b/src/cli/commands/deploy/types.ts index 44cdc7847..e005840ee 100644 --- a/src/cli/commands/deploy/types.ts +++ b/src/cli/commands/deploy/types.ts @@ -1,3 +1,5 @@ +import type { Result } from '../../../lib/types'; + export interface DeployOptions { target?: string; yes?: boolean; @@ -8,8 +10,7 @@ export interface DeployOptions { diff?: boolean; } -export interface DeployResult { - success: boolean; +export type DeployResult = Result<{ targetName?: string; stackName?: string; outputs?: Record; @@ -17,12 +18,6 @@ export interface DeployResult { nextSteps?: string[]; notes?: string[]; postDeployWarnings?: string[]; - error?: string; -} +}> & { logPath?: string }; -export interface PreflightResult { - success: boolean; - stackNames?: string[]; - needsBootstrap?: boolean; - error?: string; -} +export type PreflightResult = Result; diff --git a/src/cli/commands/dev/browser-mode.ts b/src/cli/commands/dev/browser-mode.ts index c35113e6d..9cfcc331b 100644 --- a/src/cli/commands/dev/browser-mode.ts +++ b/src/cli/commands/dev/browser-mode.ts @@ -199,7 +199,7 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { const configIO = new ConfigIO({ baseDir }); const context = await loadDeployedProjectConfig(configIO); const resolved = resolveAgent(context, { runtime: agentName }); - if (!resolved.success) return { success: false, error: resolved.error }; + if (!resolved.success) return { success: false, error: resolved.error.message }; return listTraces({ region: resolved.agent.region, runtimeId: resolved.agent.runtimeId, @@ -219,7 +219,7 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { const configIO = new ConfigIO({ baseDir }); const context = await loadDeployedProjectConfig(configIO); const resolved = resolveAgent(context, { runtime: agentName }); - if (!resolved.success) return { success: false, error: resolved.error }; + if (!resolved.success) return { success: false, error: resolved.error.message }; return fetchTraceRecords({ region: resolved.agent.region, runtimeId: resolved.agent.runtimeId, diff --git a/src/cli/commands/eval/command.tsx b/src/cli/commands/eval/command.tsx index 9dff195c4..3431138cd 100644 --- a/src/cli/commands/eval/command.tsx +++ b/src/cli/commands/eval/command.tsx @@ -33,7 +33,7 @@ export const registerEval = (program: Command) => { } if (!result.success) { - render({result.error}); + render({result.error.message}); process.exit(1); } diff --git a/src/cli/commands/import/__tests__/idempotency.test.ts b/src/cli/commands/import/__tests__/idempotency.test.ts index 4b1938e28..9b236db44 100644 --- a/src/cli/commands/import/__tests__/idempotency.test.ts +++ b/src/cli/commands/import/__tests__/idempotency.test.ts @@ -280,8 +280,8 @@ describe('Import Idempotency (Test Group 7)', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(true); - expect(result.importedAgents).toContain('my-agent'); - expect(result.importedMemories).toContain('my-memory'); + expect(result.success ? result.importedAgents : undefined).toContain('my-agent'); + expect(result.success ? result.importedMemories : undefined).toContain('my-memory'); expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(1); const writtenSpec = mockConfigIOInstance.writeProjectSpec.mock.calls[0]![0]; @@ -394,8 +394,8 @@ describe('Import Idempotency (Test Group 7)', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(true); - expect(result.importedAgents).toEqual([]); - expect(result.importedMemories).toEqual([]); + expect(result.success ? result.importedAgents : undefined).toEqual([]); + expect(result.success ? result.importedMemories : undefined).toEqual([]); }); it('does not corrupt deployed state on second import', async () => { @@ -611,7 +611,7 @@ describe('Import Idempotency (Test Group 7)', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('No agents found'); + expect(!result.success ? result.error.message : undefined).toContain('No agents found'); }); it('returns error when no project found', async () => { @@ -620,7 +620,7 @@ describe('Import Idempotency (Test Group 7)', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('No agentcore project found'); + expect(!result.success ? result.error.message : undefined).toContain('No agentcore project found'); }); }); diff --git a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts index 183a6e63f..0c44dc87b 100644 --- a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts +++ b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts @@ -178,7 +178,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(true); - expect(result.resourceId).toBe(GATEWAY_ID); + expect(result.success ? result.resourceId : undefined).toBe(GATEWAY_ID); expect(result.resourceType).toBe('gateway'); expect(result.resourceName).toBe(GATEWAY_NAME); @@ -195,12 +195,12 @@ describe('handleImportGateway', () => { describe('Rollback', () => { it('rolls back config on pipeline failure', async () => { - mockExecuteCdkImportPipeline.mockResolvedValue({ success: false, error: 'Phase 2 failed' }); + mockExecuteCdkImportPipeline.mockResolvedValue({ success: false, error: new Error('Phase 2 failed') }); const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); - expect(result.error).toBe('Phase 2 failed'); + expect(!result.success ? result.error.message : undefined).toBe('Phase 2 failed'); // First call = write merged config, second call = rollback expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); @@ -214,7 +214,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); - expect(result.error).toContain('Could not find logical ID'); + expect(!result.success ? result.error.message : undefined).toContain('Could not find logical ID'); // First call = write merged config, second call = rollback expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); @@ -232,7 +232,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); - expect(result.error).toContain('already exists'); + expect(!result.success ? result.error.message : undefined).toContain('already exists'); expect(mockConfigIOInstance.writeProjectSpec).not.toHaveBeenCalled(); }); @@ -303,7 +303,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); - expect(result.error).toContain('already exists'); + expect(!result.success ? result.error.message : undefined).toContain('already exists'); }); }); @@ -316,7 +316,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid name'); + expect(!result.success ? result.error.message : undefined).toContain('Invalid name'); expect(mockConfigIOInstance.writeProjectSpec).not.toHaveBeenCalled(); }); @@ -344,7 +344,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({}); expect(result.success).toBe(true); - expect(result.resourceId).toBe(GATEWAY_ID); + expect(result.success ? result.resourceId : undefined).toBe(GATEWAY_ID); expect(mockGetGatewayDetail).toHaveBeenCalledWith({ region: REGION, gatewayId: GATEWAY_ID }); }); @@ -357,7 +357,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({}); expect(result.success).toBe(false); - expect(result.error).toContain('Multiple gateways found'); + expect(!result.success ? result.error.message : undefined).toContain('Multiple gateways found'); }); it('fails when no gateways exist and no --arn', async () => { @@ -366,7 +366,7 @@ describe('handleImportGateway', () => { const result = await handleImportGateway({}); expect(result.success).toBe(false); - expect(result.error).toContain('No gateways found'); + expect(!result.success ? result.error.message : undefined).toContain('No gateways found'); }); }); diff --git a/src/cli/commands/import/__tests__/import-no-deploy.test.ts b/src/cli/commands/import/__tests__/import-no-deploy.test.ts index f0b84303f..a29dff3bd 100644 --- a/src/cli/commands/import/__tests__/import-no-deploy.test.ts +++ b/src/cli/commands/import/__tests__/import-no-deploy.test.ts @@ -489,10 +489,10 @@ agents: }); expect(result.success).toBe(true); - expect(result.importedAgents).toEqual([]); - expect(result.importedMemories).toEqual([]); - expect(result.stackName).toBeDefined(); - expect(result.projectSpec).toBeDefined(); + expect(result.success ? result.importedAgents : undefined).toEqual([]); + expect(result.success ? result.importedMemories : undefined).toEqual([]); + expect(result.success ? result.stackName : undefined).toBeDefined(); + expect(result.success ? result.projectSpec : undefined).toBeDefined(); }); it('emits "No deployed resources found" message', async () => { @@ -645,7 +645,7 @@ agents: const { handleImport } = await import('../actions.js'); const result = await handleImport({ source: yamlPath }); - expect(result.stackName).toBe('AgentCore-myproject-default'); + expect(result.success ? result.stackName : undefined).toBe('AgentCore-myproject-default'); }); }); @@ -725,8 +725,8 @@ agents: // No physical IDs means target resolution is skipped entirely. // The import succeeds -- config merge + source copy still happen. expect(result.success).toBe(true); - expect(result.importedAgents).toEqual([]); - expect(result.importedMemories).toEqual([]); + expect(result.success ? result.importedAgents : undefined).toEqual([]); + expect(result.success ? result.importedMemories : undefined).toEqual([]); }); it('succeeds when project already has targets even with null YAML account/region', async () => { @@ -769,8 +769,8 @@ agents: const result = await handleImport({ source: yamlPath }); expect(result.success).toBe(true); - expect(result.importedAgents).toEqual([]); - expect(result.importedMemories).toEqual([]); + expect(result.success ? result.importedAgents : undefined).toEqual([]); + expect(result.success ? result.importedMemories : undefined).toEqual([]); }); it('does not write targets when YAML has account/region but no physical IDs', async () => { @@ -816,7 +816,7 @@ agents: // No physical IDs means target is not written to disk expect(mockWriteAWSDeploymentTargets).not.toHaveBeenCalled(); // But the stackName should still be computed using 'default' fallback - expect(result.stackName).toBe('AgentCore-myproject-default'); + expect(result.success ? result.stackName : undefined).toBe('AgentCore-myproject-default'); }); }); diff --git a/src/cli/commands/import/__tests__/import-runtime-handler.test.ts b/src/cli/commands/import/__tests__/import-runtime-handler.test.ts index b4fb7de90..506470d17 100644 --- a/src/cli/commands/import/__tests__/import-runtime-handler.test.ts +++ b/src/cli/commands/import/__tests__/import-runtime-handler.test.ts @@ -25,7 +25,7 @@ const mockParseAndValidateArn = vi.fn(); const mockFindResourceInDeployedState = vi.fn(); const mockFailResult = vi.fn((...args: unknown[]) => ({ success: false, - error: args[1] as string, + error: new Error(args[1] as string), resourceType: args[2] as string, resourceName: args[3] as string, logPath: 'test.log', @@ -205,8 +205,8 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Could not determine entrypoint'); - expect(result.error).toContain('--entrypoint'); + expect(!result.success ? result.error.message : undefined).toContain('Could not determine entrypoint'); + expect(!result.success ? result.error.message : undefined).toContain('--entrypoint'); }); it('fails with clear error when entryPoint is undefined', async () => { @@ -231,7 +231,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Could not determine entrypoint'); + expect(!result.success ? result.error.message : undefined).toContain('Could not determine entrypoint'); }); it('fails with clear error when entryPoint is empty array', async () => { @@ -256,7 +256,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Could not determine entrypoint'); + expect(!result.success ? result.error.message : undefined).toContain('Could not determine entrypoint'); }); it('uses --entrypoint flag when provided, bypassing auto-detection', async () => { @@ -369,7 +369,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Multiple runtimes found'); + expect(!result.success ? result.error.message : undefined).toContain('Multiple runtimes found'); }); it('errors when no runtimes exist', async () => { @@ -382,7 +382,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('No runtimes found'); + expect(!result.success ? result.error.message : undefined).toContain('No runtimes found'); }); }); @@ -546,7 +546,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('--code'); + expect(!result.success ? result.error.message : undefined).toContain('--code'); }); it('fails when source path does not exist', async () => { @@ -574,7 +574,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('does not exist'); + expect(!result.success ? result.error.message : undefined).toContain('does not exist'); }); it('fails when runtime name already exists in project', async () => { @@ -606,7 +606,7 @@ describe('handleImportRuntime', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('already exists'); + expect(!result.success ? result.error.message : undefined).toContain('already exists'); }); }); }); diff --git a/src/cli/commands/import/__tests__/phase-failure-rollback.test.ts b/src/cli/commands/import/__tests__/phase-failure-rollback.test.ts index 149d65faf..d3bc9e4a3 100644 --- a/src/cli/commands/import/__tests__/phase-failure-rollback.test.ts +++ b/src/cli/commands/import/__tests__/phase-failure-rollback.test.ts @@ -250,7 +250,7 @@ describe('Config Rollback on Import Failure', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('Phase 1 failed'); + expect(!result.success ? result.error.message : undefined).toContain('Phase 1 failed'); // First call = merge write, second call = rollback with original (empty) runtimes expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); @@ -266,7 +266,7 @@ describe('Config Rollback on Import Failure', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('Phase 2 failed'); + expect(!result.success ? result.error.message : undefined).toContain('Phase 2 failed'); expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); const rollbackData = mockConfigIOInstance.writeProjectSpec.mock.calls[1]![0]; @@ -281,7 +281,7 @@ describe('Config Rollback on Import Failure', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('CDK build failed'); + expect(!result.success ? result.error.message : undefined).toContain('CDK build failed'); expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); const rollbackData = mockConfigIOInstance.writeProjectSpec.mock.calls[1]![0]; @@ -312,7 +312,7 @@ describe('Config Rollback on Import Failure', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('No agents found'); + expect(!result.success ? result.error.message : undefined).toContain('No agents found'); // Config was never written, so no rollback expect(mockConfigIOInstance.writeProjectSpec).not.toHaveBeenCalled(); }); @@ -339,7 +339,7 @@ describe('Config Rollback on Import Failure', () => { const result = await handleImport({ source: '/tmp/config.yaml' }); expect(result.success).toBe(false); - expect(result.error).toContain('Could not read deployed template'); + expect(!result.success ? result.error.message : undefined).toContain('Could not read deployed template'); expect(mockConfigIOInstance.writeProjectSpec).toHaveBeenCalledTimes(2); const rollbackData = mockConfigIOInstance.writeProjectSpec.mock.calls[1]![0]; diff --git a/src/cli/commands/import/actions.ts b/src/cli/commands/import/actions.ts index 71eb70f83..7352b30df 100644 --- a/src/cli/commands/import/actions.ts +++ b/src/cli/commands/import/actions.ts @@ -124,9 +124,10 @@ export async function handleImport(options: ImportOptions): Promise` first, then run import from inside the project.'; - logger.endStep('error', error); + const error = new Error( + 'No agentcore project found in the current directory.\nRun `agentcore create ` first, then run import from inside the project.' + ); + logger.endStep('error', error.message); logger.finalize(false); return { success: false, @@ -157,8 +158,8 @@ export async function handleImport(options: ImportOptions): Promise t.name === options.target); if (!found) { const names = targets.map(t => ` - ${t.name} (${t.region}, ${t.account})`).join('\n'); - const error = `Target "${options.target}" not found. Available targets:\n${names}`; - logger.endStep('error', error); + const error = new Error(`Target "${options.target}" not found. Available targets:\n${names}`); + logger.endStep('error', error.message); logger.finalize(false); return { success: false, @@ -232,8 +234,8 @@ export async function handleImport(options: ImportOptions): Promise ` - ${t.name} (${t.region}, ${t.account})`).join('\n'); - const error = `Multiple deployment targets found. Specify one with --target:\n${names}`; - logger.endStep('error', error); + const error = new Error(`Multiple deployment targets found. Specify one with --target:\n${names}`); + logger.endStep('error', error.message); logger.finalize(false); return { success: false, @@ -498,8 +500,8 @@ export async function handleImport(options: ImportOptions): Promise { console.log(`Log: ${result.logPath}`); } } else { - console.error(`\n\x1b[31m[error]${reset} Import failed: ${result.error}`); + console.error(`\n\x1b[31m[error]${reset} Import failed: ${result.error.message}`); 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 7c6c8b0d2..31a6f1d2f 100644 --- a/src/cli/commands/import/import-evaluator.ts +++ b/src/cli/commands/import/import-evaluator.ts @@ -151,7 +151,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}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); 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 3c2384e03..f78c63c9d 100644 --- a/src/cli/commands/import/import-gateway.ts +++ b/src/cli/commands/import/import-gateway.ts @@ -608,7 +608,7 @@ export async function handleImportGateway(options: ImportResourceOptions): Promi if (!pipelineResult.success) { await rollback(); - logger.endStep('error', pipelineResult.error); + logger.endStep('error', pipelineResult.error.message); logger.finalize(false); return { success: false, @@ -638,7 +638,7 @@ export async function handleImportGateway(options: ImportResourceOptions): Promi } return { success: false, - error: message, + error: new Error(message), resourceType: 'gateway', resourceName: options.name ?? '', logPath: importCtx?.logger.getRelativeLogPath(), @@ -680,7 +680,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}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); 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 2362d1353..02213c9f1 100644 --- a/src/cli/commands/import/import-memory.ts +++ b/src/cli/commands/import/import-memory.ts @@ -121,7 +121,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}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); 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 99935ecdf..b43fa174a 100644 --- a/src/cli/commands/import/import-online-eval.ts +++ b/src/cli/commands/import/import-online-eval.ts @@ -209,7 +209,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}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-pipeline.ts b/src/cli/commands/import/import-pipeline.ts index 75b9c70dd..e064a6bfe 100644 --- a/src/cli/commands/import/import-pipeline.ts +++ b/src/cli/commands/import/import-pipeline.ts @@ -1,4 +1,5 @@ import type { ConfigIO } from '../../../lib'; +import type { Result } from '../../../lib/types'; import type { AwsDeploymentTarget } from '../../../schema'; import { LocalCdkProject } from '../../cdk/local-cdk-project'; import { silentIoHost } from '../../cdk/toolkit-lib'; @@ -27,12 +28,10 @@ export interface CdkImportPipelineInput { deployedStateEntries: ImportedResource[]; } -export interface CdkImportPipelineResult { - success: boolean; - error?: string; +export type CdkImportPipelineResult = Result & { /** True when buildResourcesToImport returned an empty list. Callers decide if this is an error. */ noResources?: boolean; -} +}; /** * Shared CDK import pipeline: build → synth → bootstrap → publish assets → phase 1 → phase 2 → update state. @@ -73,7 +72,7 @@ export async function executeCdkImportPipeline(input: CdkImportPipelineInput): P const files = fs.readdirSync(assemblyDirectory).filter((f: string) => f.endsWith('.template.json')); if (files.length === 0) { await toolkitWrapper.dispose(); - return { success: false, error: 'No CloudFormation template found in CDK assembly' }; + return { success: false, error: new Error('No CloudFormation template found in CDK assembly') }; } synthTemplate = JSON.parse(fs.readFileSync(path.join(assemblyDirectory, files[0]!), 'utf-8')) as CfnTemplate; } @@ -103,14 +102,14 @@ export async function executeCdkImportPipeline(input: CdkImportPipelineInput): P }); if (!phase1Result.success) { - return { success: false, error: `Phase 1 failed: ${phase1Result.error}` }; + return { success: false, error: new Error(`Phase 1 failed: ${phase1Result.error.message}`) }; } // 6. Read deployed template onProgress('Reading deployed template...'); const deployedTemplate = await getDeployedTemplate(target.region, stackName); if (!deployedTemplate) { - return { success: false, error: 'Could not read deployed template after Phase 1' }; + return { success: false, error: new Error('Could not read deployed template after Phase 1') }; } // 7. Build resources to import (caller-specific logic) @@ -133,7 +132,7 @@ export async function executeCdkImportPipeline(input: CdkImportPipelineInput): P }); if (!phase2Result.success) { - return { success: false, error: `Phase 2 failed: ${phase2Result.error}` }; + return { success: false, error: new Error(`Phase 2 failed: ${phase2Result.error.message}`) }; } // 9. Update deployed state diff --git a/src/cli/commands/import/import-runtime.ts b/src/cli/commands/import/import-runtime.ts index b1921d546..ff6ba0640 100644 --- a/src/cli/commands/import/import-runtime.ts +++ b/src/cli/commands/import/import-runtime.ts @@ -225,7 +225,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}`); + console.error(`\n${ANSI.red}[error]${ANSI.reset} ${result.error.message}`); if (result.logPath) { console.error(`Log: ${result.logPath}`); } diff --git a/src/cli/commands/import/import-utils.ts b/src/cli/commands/import/import-utils.ts index 5829bbc6c..e29ada5ee 100644 --- a/src/cli/commands/import/import-utils.ts +++ b/src/cli/commands/import/import-utils.ts @@ -67,7 +67,7 @@ export function failResult( logger.finalize(false); return { success: false, - error, + error: new Error(error), resourceType, resourceName, logPath: logger.getRelativeLogPath(), diff --git a/src/cli/commands/import/phase1-update.ts b/src/cli/commands/import/phase1-update.ts index 38d622c6b..67128b91d 100644 --- a/src/cli/commands/import/phase1-update.ts +++ b/src/cli/commands/import/phase1-update.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { getCredentialProvider } from '../../aws/account'; import type { CfnTemplate } from './template-utils'; import { filterCompanionOnlyTemplate } from './template-utils'; @@ -18,11 +19,7 @@ export interface Phase1Options { onProgress?: (message: string) => void; } -export interface Phase1Result { - success: boolean; - stackExists: boolean; - error?: string; -} +export type Phase1Result = Result & { stackExists: boolean }; /** * Phase 1: UPDATE (pre-import) @@ -89,7 +86,7 @@ export async function executePhase1(options: Phase1Options): Promise void; } -export interface Phase2Result { - success: boolean; - error?: string; -} +export type Phase2Result = Result; /** * Phase 2: IMPORT @@ -125,11 +123,13 @@ export async function executePhase2(options: Phase2Options): Promise( if (!pipelineResult.success) { await rollback(); - logger.endStep('error', pipelineResult.error); + logger.endStep('error', pipelineResult.error.message); logger.finalize(false); return { success: false, @@ -254,7 +254,7 @@ export async function executeResourceImport( } return { success: false, - error: message, + error: new Error(message), resourceType: descriptor.resourceType, resourceName: options.name ?? '', logPath: importCtx?.logger.getRelativeLogPath(), diff --git a/src/cli/commands/import/types.ts b/src/cli/commands/import/types.ts index 5d99c79c1..8102b3c18 100644 --- a/src/cli/commands/import/types.ts +++ b/src/cli/commands/import/types.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import type { AgentCoreProjectSpec, AuthorizerConfig, @@ -89,27 +90,21 @@ export interface ResourceToImport { /** * Result of the import command. */ -export interface ImportResult { - success: boolean; - error?: string; +export type ImportResult = Result<{ projectSpec?: AgentCoreProjectSpec; importedAgents?: string[]; importedMemories?: string[]; stackName?: string; - logPath?: string; -} +}> & { logPath?: string }; /** * Result for single-resource import (runtime, memory, evaluator, etc.). */ -export interface ImportResourceResult { - success: boolean; - error?: string; +export type ImportResourceResult = Result<{ resourceId?: string }> & { resourceType: ImportableResourceType; resourceName: string; - resourceId?: string; logPath?: string; -} +}; /** * Options shared across import subcommands. diff --git a/src/cli/commands/invoke/action.ts b/src/cli/commands/invoke/action.ts index eb7aaaac2..e38e85ec7 100644 --- a/src/cli/commands/invoke/action.ts +++ b/src/cli/commands/invoke/action.ts @@ -43,41 +43,50 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption // Resolve target const targetNames = Object.keys(deployedState.targets); if (targetNames.length === 0) { - return { success: false, error: 'No deployed targets found. Run `agentcore deploy` first.' }; + return { success: false, error: new Error('No deployed targets found. Run `agentcore deploy` first.') }; } const selectedTargetName = options.targetName ?? targetNames[0]!; if (options.targetName && !targetNames.includes(options.targetName)) { - return { success: false, error: `Target '${options.targetName}' not found. Available: ${targetNames.join(', ')}` }; + return { + success: false, + error: new Error(`Target '${options.targetName}' not found. Available: ${targetNames.join(', ')}`), + }; } const targetState = deployedState.targets[selectedTargetName]; const targetConfig = awsTargets.find(t => t.name === selectedTargetName); if (!targetConfig) { - return { success: false, error: `Target config '${selectedTargetName}' not found in aws-targets` }; + return { success: false, error: new Error(`Target config '${selectedTargetName}' not found in aws-targets`) }; } if (project.runtimes.length === 0) { - return { success: false, error: 'No agents defined in configuration' }; + return { success: false, error: new Error('No agents defined in configuration') }; } // Resolve agent const agentNames = project.runtimes.map(a => a.name); if (!options.agentName && project.runtimes.length > 1) { - return { success: false, error: `Multiple runtimes found. Use --runtime to specify one: ${agentNames.join(', ')}` }; + return { + success: false, + error: new Error(`Multiple runtimes found. Use --runtime to specify one: ${agentNames.join(', ')}`), + }; } const agentSpec = options.agentName ? project.runtimes.find(a => a.name === options.agentName) : project.runtimes[0]; if (options.agentName && !agentSpec) { - return { success: false, error: `Agent '${options.agentName}' not found. Available: ${agentNames.join(', ')}` }; + return { + success: false, + error: new Error(`Agent '${options.agentName}' not found. Available: ${agentNames.join(', ')}`), + }; } if (!agentSpec) { - return { success: false, error: 'No agents defined in configuration' }; + return { success: false, error: new Error('No agents defined in configuration') }; } // Warn about VPC mode endpoint requirements @@ -91,7 +100,10 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption const agentState = targetState?.resources?.runtimes?.[agentSpec.name]; if (!agentState) { - return { success: false, error: `Agent '${agentSpec.name}' is not deployed to target '${selectedTargetName}'` }; + return { + success: false, + error: new Error(`Agent '${agentSpec.name}' is not deployed to target '${selectedTargetName}'`), + }; } // Build config bundle baggage if a bundle is associated with this agent @@ -118,13 +130,17 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption } catch (err) { return { success: false, - error: `CUSTOM_JWT agent requires a bearer token. Auto-fetch failed: ${err instanceof Error ? err.message : String(err)}\nProvide one manually with --bearer-token.`, + error: new Error( + `CUSTOM_JWT agent requires a bearer token. Auto-fetch failed: ${err instanceof Error ? err.message : String(err)}\nProvide one manually with --bearer-token.` + ), }; } } else { return { success: false, - error: `Agent '${agentSpec.name}' is configured for CUSTOM_JWT but no bearer token is available.\nEither provide --bearer-token or re-add the agent with --client-id and --client-secret to enable auto-fetch.`, + error: new Error( + `Agent '${agentSpec.name}' is configured for CUSTOM_JWT but no bearer token is available.\nEither provide --bearer-token or re-add the agent with --client-id and --client-secret to enable auto-fetch.` + ), }; } } @@ -147,7 +163,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption }); const command = options.prompt; if (!command) { - return { success: false, error: '--exec requires a command (prompt)' }; + return { success: false, error: new Error('--exec requires a command (prompt)') }; } logger.logPrompt(command, options.sessionId, options.userId); @@ -195,8 +211,15 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption logger.logResponse(stdout || stderr || `exit code: ${exitCode}`); if (options.json) { + if (exitCode !== 0) { + return { + success: false, + error: new Error(stderr || `exit code: ${exitCode}`), + logFilePath: logger.logFilePath, + }; + } return { - success: exitCode === 0, + success: true, agentName: agentSpec.name, targetName: selectedTargetName, response: JSON.stringify({ stdout, stderr, exitCode, status }), @@ -207,9 +230,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption if (exitCode === undefined) { return { success: false, - agentName: agentSpec.name, - targetName: selectedTargetName, - error: 'Command stream ended without exit code', + error: new Error('Command stream ended without exit code'), logFilePath: logger.logFilePath, }; } @@ -217,9 +238,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption if (exitCode !== 0) { return { success: false, - agentName: agentSpec.name, - targetName: selectedTargetName, - error: `Command exited with code ${exitCode}${status === 'TIMED_OUT' ? ' (timed out)' : ''}`, + error: new Error(`Command exited with code ${exitCode}${status === 'TIMED_OUT' ? ' (timed out)' : ''}`), logFilePath: logger.logFilePath, }; } @@ -261,7 +280,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption } catch (err) { return { success: false, - error: `Failed to list MCP tools: ${err instanceof Error ? err.message : String(err)}`, + error: new Error(`Failed to list MCP tools: ${err instanceof Error ? err.message : String(err)}`), }; } } @@ -271,7 +290,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption if (!options.tool) { return { success: false, - error: 'MCP call-tool requires --tool . Use "list-tools" to see available tools.', + error: new Error('MCP call-tool requires --tool . Use "list-tools" to see available tools.'), }; } let args: Record = {}; @@ -279,7 +298,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption try { args = JSON.parse(options.input) as Record; } catch { - return { success: false, error: `Invalid JSON for --input: ${options.input}` }; + return { success: false, error: new Error(`Invalid JSON for --input: ${options.input}`) }; } } try { @@ -295,7 +314,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption } catch (err) { return { success: false, - error: `Failed to call MCP tool: ${err instanceof Error ? err.message : String(err)}`, + error: new Error(`Failed to call MCP tool: ${err instanceof Error ? err.message : String(err)}`), }; } } @@ -303,14 +322,15 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption if (!options.prompt) { return { success: false, - error: - 'MCP agents require a command. Usage:\n agentcore invoke list-tools\n agentcore invoke call-tool --tool --input \'{"arg": "value"}\'', + error: new Error( + 'MCP agents require a command. Usage:\n agentcore invoke list-tools\n agentcore invoke call-tool --tool --input \'{"arg": "value"}\'' + ), }; } } if (!options.prompt) { - return { success: false, error: 'No prompt provided. Usage: agentcore invoke "your prompt"' }; + return { success: false, error: new Error('No prompt provided. Usage: agentcore invoke "your prompt"') }; } // A2A protocol handling — send JSON-RPC message/send via InvokeAgentRuntime @@ -343,7 +363,10 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption response, }; } catch (err) { - return { success: false, error: `A2A invoke failed: ${err instanceof Error ? err.message : String(err)}` }; + return { + success: false, + error: new Error(`A2A invoke failed: ${err instanceof Error ? err.message : String(err)}`), + }; } } @@ -388,8 +411,15 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption logger.logResponse(response); + if (hasError) { + return { + success: false, + error: new Error(response), + logFilePath: logger.logFilePath, + }; + } return { - success: !hasError, + success: true, agentName: agentSpec.name, targetName: selectedTargetName, response, @@ -398,7 +428,11 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption }; } catch (err) { logger.logError(err, 'AGUI invoke failed'); - return { success: false, error: `AGUI invoke failed: ${err instanceof Error ? err.message : String(err)}` }; + return { + success: false, + error: new Error(`AGUI invoke failed: ${err instanceof Error ? err.message : String(err)}`), + logFilePath: logger.logFilePath, + }; } } diff --git a/src/cli/commands/invoke/command.tsx b/src/cli/commands/invoke/command.tsx index cc0cd1e35..619b20a32 100644 --- a/src/cli/commands/invoke/command.tsx +++ b/src/cli/commands/invoke/command.tsx @@ -58,7 +58,7 @@ async function handleInvokeCLI(options: InvokeOptions): Promise { console.log(JSON.stringify(result)); } else if (options.stream) { // Streaming already wrote to stdout, just show session and log path - if (result.sessionId) { + if (result.success && result.sessionId) { console.error(`\nSession: ${result.sessionId}`); console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`); } @@ -72,7 +72,7 @@ async function handleInvokeCLI(options: InvokeOptions): Promise { } else if (!result.success && result.error) { console.error(result.error); } - if (result.sessionId) { + if (result.success && result.sessionId) { console.error(`\nSession: ${result.sessionId}`); console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`); } diff --git a/src/cli/commands/invoke/types.ts b/src/cli/commands/invoke/types.ts index 61401c332..b8c62ede1 100644 --- a/src/cli/commands/invoke/types.ts +++ b/src/cli/commands/invoke/types.ts @@ -1,3 +1,5 @@ +import type { Result } from '../../../lib/types'; + export interface InvokeOptions { agentName?: string; targetName?: string; @@ -22,12 +24,9 @@ export interface InvokeOptions { bearerToken?: string; } -export interface InvokeResult { - success: boolean; +export type InvokeResult = Result<{ agentName?: string; targetName?: string; response?: string; sessionId?: string; - error?: string; - logFilePath?: string; -} +}> & { logFilePath?: string }; diff --git a/src/cli/commands/logs/__tests__/action.test.ts b/src/cli/commands/logs/__tests__/action.test.ts index 07d072320..1cd58c625 100644 --- a/src/cli/commands/logs/__tests__/action.test.ts +++ b/src/cli/commands/logs/__tests__/action.test.ts @@ -132,9 +132,9 @@ describe('resolveAgentContext', () => { const result = resolveAgentContext(context, {}); expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('Multiple runtimes found'); - expect(result.error).toContain('AgentA'); - expect(result.error).toContain('AgentB'); + expect(result.error.message).toContain('Multiple runtimes found'); + expect(result.error.message).toContain('AgentA'); + expect(result.error.message).toContain('AgentB'); } }); @@ -205,7 +205,7 @@ describe('resolveAgentContext', () => { const result = resolveAgentContext(makeContext(), { runtime: 'UnknownAgent' }); expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("Runtime 'UnknownAgent' not found"); + expect(result.error.message).toContain("Runtime 'UnknownAgent' not found"); } }); @@ -230,7 +230,7 @@ describe('resolveAgentContext', () => { const result = resolveAgentContext(context, {}); expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('No runtimes defined'); + expect(result.error.message).toContain('No runtimes defined'); } }); @@ -249,7 +249,7 @@ describe('resolveAgentContext', () => { const result = resolveAgentContext(context, {}); expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('is not deployed'); + expect(result.error.message).toContain('is not deployed'); } }); }); diff --git a/src/cli/commands/logs/action.ts b/src/cli/commands/logs/action.ts index 72be2c864..7f5a24187 100644 --- a/src/cli/commands/logs/action.ts +++ b/src/cli/commands/logs/action.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { parseTimeString } from '../../../lib/utils'; import { searchLogs, streamLogs } from '../../aws/cloudwatch'; import { DEFAULT_ENDPOINT_NAME } from '../../constants'; @@ -17,10 +18,7 @@ export interface AgentContext { logGroupName: string; } -export interface LogsResult { - success: boolean; - error?: string; -} +export type LogsResult = Result; /** * Detect whether to stream or search based on options @@ -49,10 +47,10 @@ export function formatLogLine(event: { timestamp: number; message: string }, jso export function resolveAgentContext( context: DeployedProjectConfig, options: LogsOptions -): { success: true; agentContext: AgentContext } | { success: false; error: string } { +): Result<{ agentContext: AgentContext }> { const result = resolveAgent(context, options); if (!result.success) { - return { success: false, error: result.error }; + return result; } const { agent } = result; const endpointName = DEFAULT_ENDPOINT_NAME; @@ -78,7 +76,7 @@ export async function handleLogs(options: LogsOptions): Promise { if (options.level && !VALID_LEVELS.includes(options.level.toLowerCase())) { return { success: false, - error: `Invalid log level: "${options.level}". Valid levels: ${VALID_LEVELS.join(', ')}`, + error: new Error(`Invalid log level: "${options.level}". Valid levels: ${VALID_LEVELS.join(', ')}`), }; } @@ -96,7 +94,7 @@ export async function handleLogs(options: LogsOptions): Promise { try { filterPattern = buildFilterPattern({ level: options.level, query: options.query }); } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: err as Error }; } const mode = detectMode(options); @@ -143,7 +141,7 @@ export async function handleLogs(options: LogsOptions): Promise { if (errorName === 'ResourceNotFoundException') { return { success: false, - error: `No logs found for agent '${agentContext.agentName}'. Has the agent been invoked?`, + error: new Error(`No logs found for agent '${agentContext.agentName}'. Has the agent been invoked?`), }; } diff --git a/src/cli/commands/logs/command.tsx b/src/cli/commands/logs/command.tsx index 05649887d..ba6d1ef21 100644 --- a/src/cli/commands/logs/command.tsx +++ b/src/cli/commands/logs/command.tsx @@ -34,7 +34,7 @@ export const registerLogs = (program: Command) => { const result = await handleLogs(cliOptions); if (!result.success) { - render({result.error}); + render({result.error.message}); process.exit(1); } } catch (error) { @@ -59,7 +59,7 @@ export const registerLogs = (program: Command) => { const result = await handleLogsEval(cliOptions); if (!result.success) { - render({result.error}); + render({result.error.message}); process.exit(1); } } catch (error) { diff --git a/src/cli/commands/package/action.ts b/src/cli/commands/package/action.ts index 5a99baa05..7c528b492 100644 --- a/src/cli/commands/package/action.ts +++ b/src/cli/commands/package/action.ts @@ -6,6 +6,7 @@ import { resolveCodeLocation, validateAgentExists, } from '../../../lib'; +import type { Result } from '../../../lib/types'; import type { AgentCoreProjectSpec } from '../../../schema'; import { join, resolve } from 'path'; @@ -40,12 +41,7 @@ export interface PackageAgentResult { sizeMb: string; } -export interface PackageResult { - success: boolean; - results: PackageAgentResult[]; - skipped: string[]; - error?: string; -} +export type PackageResult = Result & { results: PackageAgentResult[]; skipped: string[] }; export async function handlePackage(context: PackageContext): Promise { const { project, configBaseDir, targetAgent } = context; diff --git a/src/cli/commands/pause/command.tsx b/src/cli/commands/pause/command.tsx index 82a79bccf..e72fe032f 100644 --- a/src/cli/commands/pause/command.tsx +++ b/src/cli/commands/pause/command.tsx @@ -55,7 +55,7 @@ function registerOnlineEvalSubcommand(parent: Command, action: 'pause' | 'resume const displayName = cliOptions.arn ? result.configId : name; console.log(`${pastTense} online eval config "${displayName}" (status: ${result.executionStatus})`); } else { - render({result.error}); + render({result.error.message}); } process.exit(result.success ? 0 : 1); diff --git a/src/cli/commands/remove/command.tsx b/src/cli/commands/remove/command.tsx index 05e532688..ca26b2826 100644 --- a/src/cli/commands/remove/command.tsx +++ b/src/cli/commands/remove/command.tsx @@ -48,14 +48,14 @@ async function handleRemoveAll(_options: RemoveAllOptions): Promise { validateRemoveAllOptions(options); const result = await handleRemoveAll(options); - console.log(JSON.stringify(result)); + console.log(JSON.stringify(result.success ? result : { ...result, error: result.error.message })); process.exit(result.success ? 0 : 1); } diff --git a/src/cli/commands/remove/types.ts b/src/cli/commands/remove/types.ts index 9a30f5e24..c6832c121 100644 --- a/src/cli/commands/remove/types.ts +++ b/src/cli/commands/remove/types.ts @@ -1,3 +1,5 @@ +import type { Result } from '../../../lib/types'; + export type ResourceType = | 'agent' | 'gateway' @@ -25,11 +27,9 @@ export interface RemoveAllOptions { json?: boolean; } -export interface RemoveResult { - success: boolean; +export type RemoveResult = Result<{ resourceType?: ResourceType; resourceName?: string; message?: string; note?: string; - error?: string; -} +}>; diff --git a/src/cli/commands/run/command.tsx b/src/cli/commands/run/command.tsx index e5ba8ca59..7afc646e1 100644 --- a/src/cli/commands/run/command.tsx +++ b/src/cli/commands/run/command.tsx @@ -25,7 +25,7 @@ const RECOMMENDATION_TYPE_MAP: Record = { }; function formatRunOutput(result: Awaited>): void { - if (!result.run) return; + if (!result.success) return; const { run } = result; const date = new Date(run.timestamp).toLocaleString([], { @@ -151,7 +151,7 @@ export const registerRun = (program: Command) => { formatRunOutput(result); } else { formatRunOutput(result); - render({result.error}); + render({result.error.message}); } process.exit(result.success ? 0 : 1); @@ -244,7 +244,7 @@ export const registerRun = (program: Command) => { } else if (result.success) { formatBatchEvalOutput(result); } else { - render({result.error}); + render({result.error.message}); if (result.logFilePath) { console.error(`\nLog: ${result.logFilePath}`); } @@ -403,7 +403,7 @@ export const registerRun = (program: Command) => { if (cliOptions.json) { console.log(JSON.stringify(result)); } else { - render({result.error}); + render({result.error.message}); if (result.logFilePath) { console.error(`\nLog: ${result.logFilePath}`); } @@ -495,7 +495,7 @@ function formatBatchEvalOutput(result: RunBatchEvaluationCommandResult): void { console.log(`Status: ${result.status}`); // Show session stats from API if available - const evalResults = result.evaluationResults; + const evalResults = result.success ? result.evaluationResults : undefined; if (evalResults) { const parts: string[] = []; if (evalResults.totalNumberOfSessions != null) parts.push(`${evalResults.totalNumberOfSessions} sessions`); diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index 271b05bc9..bd5a121bc 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -1,4 +1,5 @@ import { ConfigIO } from '../../../lib'; +import type { Result } from '../../../lib/types'; import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedResourceState, DeployedState } from '../../../schema'; import { getAgentRuntimeStatus } from '../../aws'; import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-control'; @@ -32,15 +33,12 @@ export interface ResourceStatusEntry { invocationUrl?: string; } -export interface ProjectStatusResult { - success: boolean; +export type ProjectStatusResult = Result<{ targetRegion?: string }> & { projectName: string; targetName: string; - targetRegion?: string; resources: ResourceStatusEntry[]; - error?: string; logPath?: string; -} +}; export interface StatusContext { project: AgentCoreProjectSpec; @@ -48,14 +46,11 @@ export interface StatusContext { awsTargets: AwsDeploymentTargets; } -export interface RuntimeLookupResult { - success: boolean; +export type RuntimeLookupResult = Result<{ targetName?: string; runtimeId?: string; runtimeStatus?: string; - error?: string; - logPath?: string; -} +}> & { logPath?: string }; /** * Loads configuration required for status check. @@ -336,7 +331,7 @@ export async function handleProjectStatus( projectName: project.name, targetName: options.targetName, resources: [], - error, + error: new Error(error), logPath: logger.getRelativeLogPath(), }; } @@ -504,7 +499,7 @@ export async function handleRuntimeLookup( const error = 'No deployment targets found. Run `agentcore create` first.'; logger.endStep('error', error); logger.finalize(false); - return { success: false, error, logPath: logger.getRelativeLogPath() }; + return { success: false, error: new Error(error), logPath: logger.getRelativeLogPath() }; } const selectedTargetName = options.targetName ?? targetNames[0]!; @@ -513,7 +508,7 @@ export async function handleRuntimeLookup( const error = `Target '${options.targetName}' not found. Available: ${targetNames.join(', ')}`; logger.endStep('error', error); logger.finalize(false); - return { success: false, error, logPath: logger.getRelativeLogPath() }; + return { success: false, error: new Error(error), logPath: logger.getRelativeLogPath() }; } const targetConfig = awsTargets.find(target => target.name === selectedTargetName); @@ -522,7 +517,7 @@ export async function handleRuntimeLookup( const error = `Target config '${selectedTargetName}' not found in aws-targets`; logger.endStep('error', error); logger.finalize(false); - return { success: false, error, logPath: logger.getRelativeLogPath() }; + return { success: false, error: new Error(error), logPath: logger.getRelativeLogPath() }; } logger.log(`Target: ${selectedTargetName} (${targetConfig.region})`); @@ -550,6 +545,6 @@ export async function handleRuntimeLookup( const errorMsg = getErrorMessage(error); logger.endStep('error', errorMsg); logger.finalize(false); - return { success: false, error: errorMsg, logPath: logger.getRelativeLogPath() }; + return { success: false, error: new Error(errorMsg), logPath: logger.getRelativeLogPath() }; } } diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index 506ad10ec..083df19a6 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -105,7 +105,7 @@ export const registerStatus = (program: Command) => { } if (!result.success) { - render({result.error}); + render({result.error.message}); return; } @@ -132,7 +132,7 @@ export const registerStatus = (program: Command) => { } if (!result.success) { - render({result.error}); + render({result.error.message}); return; } diff --git a/src/cli/commands/traces/action.ts b/src/cli/commands/traces/action.ts index d761cd4b7..8c5fe3c65 100644 --- a/src/cli/commands/traces/action.ts +++ b/src/cli/commands/traces/action.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { parseTimeString } from '../../../lib/utils'; import type { DeployedProjectConfig } from '../../operations/resolve-agent'; import { resolveAgent } from '../../operations/resolve-agent'; @@ -19,7 +20,7 @@ export async function handleTracesList( ): Promise { const resolved = resolveAgent(context, options); if (!resolved.success) { - return { success: false, error: resolved.error }; + return { success: false, error: resolved.error.message }; } const { agent } = resolved; @@ -56,7 +57,7 @@ export async function handleTracesList( }); if (!result.success) { - return { success: false, error: result.error, consoleUrl }; + return { success: false, error: result.error.message, consoleUrl }; } return { @@ -68,14 +69,11 @@ export async function handleTracesList( }; } -export interface TracesGetResult { - success: boolean; +export type TracesGetResult = Result<{ agentName?: string; targetName?: string; - consoleUrl?: string; filePath?: string; - error?: string; -} +}> & { consoleUrl?: string }; export async function handleTracesGet( context: DeployedProjectConfig, diff --git a/src/cli/commands/traces/command.tsx b/src/cli/commands/traces/command.tsx index 0222ce419..f9aa2f8b6 100644 --- a/src/cli/commands/traces/command.tsx +++ b/src/cli/commands/traces/command.tsx @@ -109,7 +109,7 @@ export const registerTraces = (program: Command) => { if (!result.success) { render( - Error: {result.error} + Error: {result.error.message} {result.consoleUrl && Console: {result.consoleUrl}} ); diff --git a/src/cli/commands/validate/__tests__/action.test.ts b/src/cli/commands/validate/__tests__/action.test.ts index d6774f252..1685f8dd9 100644 --- a/src/cli/commands/validate/__tests__/action.test.ts +++ b/src/cli/commands/validate/__tests__/action.test.ts @@ -74,7 +74,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('No agentcore project found'); + expect(!result.success ? result.error.message : undefined).toContain('No agentcore project found'); }); it('returns success when all configs are valid', async () => { @@ -95,7 +95,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('invalid project'); + expect(!result.success ? result.error.message : undefined).toContain('invalid project'); }); it('returns error when AWS targets fails', async () => { @@ -106,7 +106,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('bad targets'); + expect(!result.success ? result.error.message : undefined).toContain('bad targets'); }); it('validates state file when it exists', async () => { @@ -132,7 +132,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('bad state'); + expect(!result.success ? result.error.message : undefined).toContain('bad state'); }); it('uses custom directory when provided', async () => { @@ -155,7 +155,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toBe('field "name" is required'); + expect(!result.success ? result.error.message : undefined).toBe('field "name" is required'); }); it('formats ConfigParseError with cause', async () => { @@ -166,8 +166,8 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid JSON in agentcore.json'); - expect(result.error).toContain('Unexpected token'); + expect(!result.success ? result.error.message : undefined).toContain('Invalid JSON in agentcore.json'); + expect(!result.success ? result.error.message : undefined).toContain('Unexpected token'); }); it('formats ConfigReadError with cause', async () => { @@ -180,8 +180,8 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toContain('Failed to read agentcore.json'); - expect(result.error).toContain('EACCES'); + expect(!result.success ? result.error.message : undefined).toContain('Failed to read agentcore.json'); + expect(!result.success ? result.error.message : undefined).toContain('EACCES'); }); it('formats ConfigNotFoundError with file name', async () => { @@ -192,7 +192,7 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toBe('Required file not found: agentcore.json'); + expect(!result.success ? result.error.message : undefined).toBe('Required file not found: agentcore.json'); }); it('formats non-Error values as strings', async () => { @@ -202,6 +202,6 @@ describe('handleValidate', () => { const result = await handleValidate({}); expect(result.success).toBe(false); - expect(result.error).toBe('string error'); + expect(!result.success ? result.error.message : undefined).toBe('string error'); }); }); diff --git a/src/cli/commands/validate/action.ts b/src/cli/commands/validate/action.ts index 572f5cf7d..99031c996 100644 --- a/src/cli/commands/validate/action.ts +++ b/src/cli/commands/validate/action.ts @@ -7,15 +7,13 @@ import { NoProjectError, findConfigRoot, } from '../../../lib'; +import type { Result } from '../../../lib/types'; export interface ValidateOptions { directory?: string; } -export interface ValidateResult { - success: boolean; - error?: string; -} +export type ValidateResult = Result; /** * Validates all AgentCore schema files in the project. @@ -29,7 +27,7 @@ export async function handleValidate(options: ValidateOptions): Promise { render(Valid); process.exit(0); } else { - render({result.error}); + render({result.error.message}); process.exit(1); } }); diff --git a/src/cli/operations/agent/import/index.ts b/src/cli/operations/agent/import/index.ts index e49d9e3c0..e3226bdba 100644 --- a/src/cli/operations/agent/import/index.ts +++ b/src/cli/operations/agent/import/index.ts @@ -9,7 +9,7 @@ import { getBedrockAgentConfig } from '../../../aws/bedrock-import'; import { getErrorMessage } from '../../../errors'; import type { JwtConfigOptions } from '../../../primitives/auth-utils'; import { createManagedOAuthCredential } from '../../../primitives/auth-utils'; -import type { AddResult } from '../../../primitives/types'; +import type { Result } from '../../../primitives/types'; import type { MemoryOption } from '../../../tui/screens/generate/types'; import { setupPythonProject } from '../../python'; import { writeAgentToProject } from '../generate/write-agent-to-project'; @@ -36,7 +36,7 @@ export interface ExecuteImportAgentParams { export async function executeImportAgent( params: ExecuteImportAgentParams -): Promise> { +): Promise> { const { name, framework, @@ -120,6 +120,6 @@ export async function executeImportAgent( return { success: true, agentName: name, agentPath }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } diff --git a/src/cli/operations/deploy/post-deploy-observability.ts b/src/cli/operations/deploy/post-deploy-observability.ts index 0616a65dc..b153a376e 100644 --- a/src/cli/operations/deploy/post-deploy-observability.ts +++ b/src/cli/operations/deploy/post-deploy-observability.ts @@ -1,4 +1,5 @@ import { readGlobalConfigSync } from '../../../lib/schemas/io/global-config'; +import type { Result } from '../../../lib/types'; import { enableTransactionSearch } from '../../aws/transaction-search'; export interface TransactionSearchSetupOptions { @@ -8,10 +9,7 @@ export interface TransactionSearchSetupOptions { hasGateways?: boolean; } -export interface TransactionSearchSetupResult { - success: boolean; - error?: string; -} +export type TransactionSearchSetupResult = Result; /** * Post-deploy step: enable CloudWatch Transaction Search (Application Signals + diff --git a/src/cli/operations/deploy/teardown.ts b/src/cli/operations/deploy/teardown.ts index 2e38f2576..8373448e3 100644 --- a/src/cli/operations/deploy/teardown.ts +++ b/src/cli/operations/deploy/teardown.ts @@ -1,4 +1,5 @@ import { CONFIG_DIR, ConfigIO } from '../../../lib'; +import type { Result } from '../../../lib/types'; import type { AwsDeploymentTarget } from '../../../schema'; import { withTargetRegion } from '../../aws'; import { deleteConfigurationBundle } from '../../aws/agentcore-config-bundles'; @@ -99,10 +100,7 @@ export function getCdkProjectDir(cwd?: string): string { return join(baseDir, CONFIG_DIR, 'cdk'); } -export interface StackTeardownResult { - success: boolean; - error?: string; -} +export type StackTeardownResult = Result; /** * Perform full stack teardown for a target: destroy CloudFormation stack, diff --git a/src/cli/operations/dev/web-ui/web-server.ts b/src/cli/operations/dev/web-ui/web-server.ts index 2b20b2d07..c7fa72e19 100644 --- a/src/cli/operations/dev/web-ui/web-server.ts +++ b/src/cli/operations/dev/web-ui/web-server.ts @@ -89,7 +89,7 @@ export type ListCloudWatchTracesHandler = ( harnessName: string | undefined, startTime?: number, endTime?: number -) => Promise<{ success: boolean; traces?: unknown[]; error?: string }>; +) => Promise<{ success: boolean; traces?: unknown[]; error?: string | Error }>; /** * Custom handler for GET /api/cloudwatch-traces/:traceId. @@ -101,7 +101,7 @@ export type GetCloudWatchTraceHandler = ( traceId: string, startTime?: number, endTime?: number -) => Promise<{ success: boolean; records?: unknown[]; spans?: unknown[]; error?: string }>; +) => Promise<{ success: boolean; records?: unknown[]; spans?: unknown[]; error?: string | Error }>; /** * Custom handler for GET /api/memory. @@ -111,7 +111,7 @@ export type ListMemoryRecordsHandler = ( memoryName: string, namespace: string, strategyId?: string -) => Promise<{ success: boolean; records?: unknown[]; error?: string }>; +) => Promise<{ success: boolean; records?: unknown[]; error?: string | Error }>; /** * Custom handler for POST /api/memory/search. @@ -122,7 +122,7 @@ export type RetrieveMemoryRecordsHandler = ( namespace: string, searchQuery: string, strategyId?: string -) => Promise<{ success: boolean; records?: unknown[]; error?: string }>; +) => Promise<{ success: boolean; records?: unknown[]; error?: string | Error }>; export interface WebUIOptions { /** Server mode identifier (currently only 'dev' is used) */ diff --git a/src/cli/operations/eval/__tests__/list-eval-runs.test.ts b/src/cli/operations/eval/__tests__/list-eval-runs.test.ts index c9a71a8cf..4bcf69339 100644 --- a/src/cli/operations/eval/__tests__/list-eval-runs.test.ts +++ b/src/cli/operations/eval/__tests__/list-eval-runs.test.ts @@ -29,6 +29,7 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({}); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toHaveLength(2); }); @@ -43,7 +44,9 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({ agent: 'agent-a' }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toHaveLength(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.runs!.every(r => r.agent === 'agent-a')).toBe(true); }); @@ -58,6 +61,7 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({ limit: 2 }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toHaveLength(2); }); @@ -72,8 +76,11 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({ agent: 'a', limit: 2 }); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toHaveLength(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.runs![0]!.timestamp).toBe('2025-01-15T10:00:00.000Z'); + // @ts-expect-error -- test accesses discriminated union field expect(result.runs![1]!.timestamp).toBe('2025-01-15T12:00:00.000Z'); }); @@ -83,6 +90,7 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({}); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toEqual([]); }); @@ -94,7 +102,9 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({}); expect(result.success).toBe(false); - expect(result.error).toBe('disk error'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('disk error'); + // @ts-expect-error -- test accesses success-branch field expect(result.runs).toBeUndefined(); }); @@ -106,6 +116,7 @@ describe('handleListEvalRuns', () => { const result = handleListEvalRuns({}); expect(result.success).toBe(false); - expect(result.error).toBe('42'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('42'); }); }); diff --git a/src/cli/operations/eval/__tests__/logs-eval.test.ts b/src/cli/operations/eval/__tests__/logs-eval.test.ts index 5411d842e..69b5adbb6 100644 --- a/src/cli/operations/eval/__tests__/logs-eval.test.ts +++ b/src/cli/operations/eval/__tests__/logs-eval.test.ts @@ -96,12 +96,13 @@ describe('handleLogsEval', () => { it('returns error when agent resolution fails', async () => { mockLoadDeployedProjectConfig.mockResolvedValue({}); - mockResolveAgent.mockReturnValue({ success: false, error: 'No agents defined' }); + mockResolveAgent.mockReturnValue({ success: false, error: new Error('No agents defined') }); const result = await handleLogsEval({}); expect(result.success).toBe(false); - expect(result.error).toBe('No agents defined'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('No agents defined'); }); it('returns error when no online eval configs exist for the agent', async () => { @@ -112,7 +113,8 @@ describe('handleLogsEval', () => { const result = await handleLogsEval({}); expect(result.success).toBe(false); - expect(result.error).toContain('No deployed online eval configs found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No deployed online eval configs found'); }); it('returns error when online eval configs exist but none are deployed', async () => { @@ -123,7 +125,8 @@ describe('handleLogsEval', () => { const result = await handleLogsEval({}); expect(result.success).toBe(false); - expect(result.error).toContain('No deployed online eval configs found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No deployed online eval configs found'); }); it('searches logs with time range when --since is specified', async () => { diff --git a/src/cli/operations/eval/__tests__/pause-resume.test.ts b/src/cli/operations/eval/__tests__/pause-resume.test.ts index 834525675..ee31bc1b9 100644 --- a/src/cli/operations/eval/__tests__/pause-resume.test.ts +++ b/src/cli/operations/eval/__tests__/pause-resume.test.ts @@ -47,6 +47,7 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'my-config' }, 'pause'); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.executionStatus).toBe('DISABLED'); expect(mockUpdateOnlineEvalExecutionStatus).toHaveBeenCalledWith({ region: 'us-east-1', @@ -66,6 +67,7 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'my-config' }, 'resume'); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.executionStatus).toBe('ENABLED'); expect(mockUpdateOnlineEvalExecutionStatus).toHaveBeenCalledWith({ region: 'us-east-1', @@ -84,7 +86,8 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'my-config' }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toContain('No deployed targets found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No deployed targets found'); }); it('returns error when config name is not found in deployed state', async () => { @@ -93,8 +96,10 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'missing-config' }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toContain('missing-config'); - expect(result.error).toContain('not found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('missing-config'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not found'); }); it('returns error when target config is missing from aws-targets', async () => { @@ -106,8 +111,10 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'my-config' }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toContain('Target config'); - expect(result.error).toContain('not found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Target config'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not found'); }); it('returns error when the SDK call fails', async () => { @@ -117,7 +124,8 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: 'my-config' }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toBe('Service unavailable'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('Service unavailable'); }); describe('ARN mode', () => { @@ -132,6 +140,7 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: '', arn }, 'pause'); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.executionStatus).toBe('DISABLED'); expect(mockLoadDeployedProjectConfig).not.toHaveBeenCalled(); expect(mockUpdateOnlineEvalExecutionStatus).toHaveBeenCalledWith({ @@ -152,6 +161,7 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: '', arn, region: 'eu-west-1' }, 'resume'); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.executionStatus).toBe('ENABLED'); expect(mockUpdateOnlineEvalExecutionStatus).toHaveBeenCalledWith({ region: 'eu-west-1', @@ -164,7 +174,8 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: '', arn: 'not-an-arn' }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid online eval config ARN'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Invalid online eval config ARN'); }); it('returns error when config ID cannot be extracted from ARN', async () => { @@ -172,7 +183,8 @@ describe('handlePauseResume', () => { const result = await handlePauseResume({ name: '', arn }, 'pause'); expect(result.success).toBe(false); - expect(result.error).toContain('Could not extract config ID'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Could not extract config ID'); }); }); }); diff --git a/src/cli/operations/eval/__tests__/run-eval.test.ts b/src/cli/operations/eval/__tests__/run-eval.test.ts index 39314af69..0d8edcf30 100644 --- a/src/cli/operations/eval/__tests__/run-eval.test.ts +++ b/src/cli/operations/eval/__tests__/run-eval.test.ts @@ -146,12 +146,13 @@ describe('handleRunEval', () => { it('returns error when agent resolution fails', async () => { mockLoadDeployedProjectConfig.mockResolvedValue({}); - mockResolveAgent.mockReturnValue({ success: false, error: 'No agents defined' }); + mockResolveAgent.mockReturnValue({ success: false, error: new Error('No agents defined') }); const result = await handleRunEval({ evaluator: ['Builtin.GoalSuccessRate'], days: 7 }); expect(result.success).toBe(false); - expect(result.error).toBe('No agents defined'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('No agents defined'); }); it('returns error when a custom evaluator is not found in deployed state', async () => { @@ -171,8 +172,10 @@ describe('handleRunEval', () => { const result = await handleRunEval({ evaluator: ['MissingEval'], days: 7 }); expect(result.success).toBe(false); - expect(result.error).toContain('MissingEval'); - expect(result.error).toContain('not found in deployed state'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('MissingEval'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not found in deployed state'); }); it('resolves builtin evaluators without deployed state lookup', async () => { @@ -195,7 +198,8 @@ describe('handleRunEval', () => { const result = await handleRunEval({ evaluator: ['Builtin.GoalSuccessRate'], days: 7 }); // Fails because no spans, but NOT because evaluator wasn't found - expect(result.error).toContain('No session spans found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No session spans found'); }); it('resolves custom evaluator name to deployed evaluator ID', async () => { @@ -279,8 +283,10 @@ describe('handleRunEval', () => { const result = await handleRunEval({ evaluator: ['Builtin.GoalSuccessRate'], days: 7 }); expect(result.success).toBe(false); - expect(result.error).toContain('No session spans found'); - expect(result.error).toContain('my-agent'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No session spans found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('my-agent'); }); // ─── Successful evaluation ──────────────────────────────────────────────── @@ -325,10 +331,14 @@ describe('handleRunEval', () => { const result = await handleRunEval({ evaluator: ['Builtin.GoalSuccessRate'], days: 7 }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.run).toBeDefined(); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.sessionCount).toBe(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results).toHaveLength(1); + // @ts-expect-error -- test accesses discriminated union field const evalResult = result.run!.results[0]!; expect(evalResult.aggregateScore).toBe(3.0); // (4 + 2) / 2 expect(evalResult.sessionScores).toHaveLength(2); @@ -362,6 +372,7 @@ describe('handleRunEval', () => { const result = await handleRunEval({ evaluator: ['Builtin.GoalSuccessRate'], days: 7 }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field const evalResult = result.run!.results[0]!; // Only the non-errored session (value 5.0) should be in the aggregate expect(evalResult.aggregateScore).toBe(5.0); @@ -394,6 +405,7 @@ describe('handleRunEval', () => { expect(result.success).toBe(true); expect(mockSaveEvalRun).toHaveBeenCalled(); expect(mockWriteFileSync).not.toHaveBeenCalled(); + // @ts-expect-error -- test accesses success-branch field expect(result.filePath).toBe('/tmp/eval-results/eval_2025-01-15_10-00-00.json'); }); @@ -425,6 +437,7 @@ describe('handleRunEval', () => { expect(result.success).toBe(true); expect(mockWriteFileSync).toHaveBeenCalledWith('/tmp/my-output.json', expect.any(String)); expect(mockSaveEvalRun).not.toHaveBeenCalled(); + // @ts-expect-error -- test accesses success-branch field expect(result.filePath).toBe('/tmp/my-output.json'); }); @@ -462,10 +475,15 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results).toHaveLength(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results[0]!.evaluator).toBe('Builtin.GoalSuccessRate'); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results[0]!.aggregateScore).toBe(0.9); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results[1]!.evaluator).toBe('CustomEval'); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.results[1]!.aggregateScore).toBe(4.5); }); @@ -485,6 +503,7 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.agent).toBe('rt-arn-test'); expect(mockLoadDeployedProjectConfig).not.toHaveBeenCalled(); expect(mockResolveAgent).not.toHaveBeenCalled(); @@ -533,7 +552,8 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid agent runtime ARN'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Invalid agent runtime ARN'); }); it('rejects custom evaluator names in ARN mode', async () => { @@ -544,7 +564,8 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('cannot be resolved in ARN mode'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('cannot be resolved in ARN mode'); }); it('saves to cwd in ARN mode when no --output is specified', async () => { @@ -566,6 +587,7 @@ describe('handleRunEval', () => { expect.stringContaining('eval_2025-01-15_10-00-00.json'), expect.any(String) ); + // @ts-expect-error -- test accesses success-branch field expect(result.filePath).toContain('eval_2025-01-15_10-00-00.json'); }); @@ -584,6 +606,7 @@ describe('handleRunEval', () => { expect(result.success).toBe(true); expect(mockWriteFileSync).toHaveBeenCalledWith('/tmp/custom-eval.json', expect.any(String)); + // @ts-expect-error -- test accesses success-branch field expect(result.filePath).toBe('/tmp/custom-eval.json'); }); @@ -595,7 +618,8 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('No evaluators specified'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No evaluators specified'); }); // ─── Endpoint selection ────────────────────────────────────────────────── @@ -1200,6 +1224,7 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field expect(result.run!.referenceInputs).toEqual({ expectedTrajectory: ['tool_1', 'tool_2'], }); @@ -1216,6 +1241,7 @@ describe('handleRunEval', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('require exactly one session'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('require exactly one session'); }); }); diff --git a/src/cli/operations/eval/batch-eval-storage.ts b/src/cli/operations/eval/batch-eval-storage.ts index 3145120ba..7296c8aaa 100644 --- a/src/cli/operations/eval/batch-eval-storage.ts +++ b/src/cli/operations/eval/batch-eval-storage.ts @@ -40,7 +40,7 @@ export function saveBatchEvalRun(result: RunBatchEvaluationCommandResult): strin completedAt: result.completedAt, evaluators: result.results.map(r => r.evaluatorId), results: result.results, - evaluationResults: result.evaluationResults, + evaluationResults: result.success ? result.evaluationResults : undefined, }; writeFileSync(filePath, JSON.stringify(record, null, 2)); diff --git a/src/cli/operations/eval/list-eval-runs.ts b/src/cli/operations/eval/list-eval-runs.ts index 66b0ed528..96674a0c8 100644 --- a/src/cli/operations/eval/list-eval-runs.ts +++ b/src/cli/operations/eval/list-eval-runs.ts @@ -1,12 +1,9 @@ +import type { Result } from '../../../lib/types'; import { getErrorMessage } from '../../errors'; import { listEvalRuns } from './storage'; import type { EvalRunResult, ListEvalRunsOptions } from './types'; -export interface ListEvalRunsResult { - success: boolean; - error?: string; - runs?: EvalRunResult[]; -} +export type ListEvalRunsResult = Result<{ runs: EvalRunResult[] }>; export function handleListEvalRuns(options: ListEvalRunsOptions): ListEvalRunsResult { try { @@ -22,6 +19,6 @@ export function handleListEvalRuns(options: ListEvalRunsOptions): ListEvalRunsRe return { success: true, runs }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: new Error(getErrorMessage(err)) }; } } diff --git a/src/cli/operations/eval/logs-eval.ts b/src/cli/operations/eval/logs-eval.ts index 430103a44..070993a3e 100644 --- a/src/cli/operations/eval/logs-eval.ts +++ b/src/cli/operations/eval/logs-eval.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { parseTimeString } from '../../../lib/utils'; import { getOnlineEvaluationConfig } from '../../aws/agentcore-control'; import { searchLogs, streamLogs } from '../../aws/cloudwatch'; @@ -13,10 +14,7 @@ export interface LogsEvalOptions { follow?: boolean; } -export interface LogsEvalResult { - success: boolean; - error?: string; -} +export type LogsEvalResult = Result; function formatLogLine(event: { timestamp: number; message: string }, json: boolean): string { if (json) { @@ -75,7 +73,7 @@ export async function handleLogsEval(options: LogsEvalOptions): Promise; -async function resolveOnlineEvalConfig( - configName: string -): Promise<{ success: true; configId: string; region: string } | { success: false; error: string }> { +async function resolveOnlineEvalConfig(configName: string): Promise> { const context = await loadDeployedProjectConfig(); const targetNames = Object.keys(context.deployedState.targets); if (targetNames.length === 0) { - return { success: false, error: 'No deployed targets found. Run `agentcore deploy` first.' }; + return { success: false, error: new Error('No deployed targets found. Run `agentcore deploy` first.') }; } const targetName = targetNames[0]!; @@ -27,13 +21,13 @@ async function resolveOnlineEvalConfig( if (!deployedConfig) { return { success: false, - error: `Online eval config "${configName}" not found in deployed state. Has it been deployed?`, + error: new Error(`Online eval config "${configName}" not found in deployed state. Has it been deployed?`), }; } const targetConfig = context.awsTargets.find(t => t.name === targetName); if (!targetConfig) { - return { success: false, error: `Target config "${targetName}" not found in aws-targets.` }; + return { success: false, error: new Error(`Target config "${targetName}" not found in aws-targets.`) }; } return { @@ -47,24 +41,21 @@ async function resolveOnlineEvalConfig( * Parse an online eval config ARN to extract the config ID and region. * ARN format: arn:aws:bedrock-agentcore:::online-evaluation-config/ */ -function parseOnlineEvalConfigArn( - arn: string, - regionOverride?: string -): { success: true; configId: string; region: string } | { success: false; error: string } { +function parseOnlineEvalConfigArn(arn: string, regionOverride?: string): Result<{ configId: string; region: string }> { const parts = arn.split(':'); if (parts.length < 6 || !arn.startsWith('arn:')) { - return { success: false, error: `Invalid online eval config ARN: ${arn}` }; + return { success: false, error: new Error(`Invalid online eval config ARN: ${arn}`) }; } const region = regionOverride ?? parts[3]; if (!region) { - return { success: false, error: 'Could not determine region from ARN. Use --region to specify.' }; + return { success: false, error: new Error('Could not determine region from ARN. Use --region to specify.') }; } const resource = parts.slice(5).join(':'); const match = /online-evaluation-config\/(.+)$/.exec(resource); if (!match) { - return { success: false, error: `Could not extract config ID from ARN: ${arn}` }; + return { success: false, error: new Error(`Could not extract config ID from ARN: ${arn}`) }; } return { success: true, configId: match[1]!, region }; @@ -73,9 +64,7 @@ function parseOnlineEvalConfigArn( /** * Resolve config ID and region from either a project config name or an ARN. */ -async function resolveConfig( - options: OnlineEvalActionOptions -): Promise<{ success: true; configId: string; region: string } | { success: false; error: string }> { +async function resolveConfig(options: OnlineEvalActionOptions): Promise> { if (options.arn) { return parseOnlineEvalConfigArn(options.arn, options.region); } @@ -88,7 +77,7 @@ export async function handlePauseResume( ): Promise { const resolution = await resolveConfig(options); if (!resolution.success) { - return resolution; + return { success: false, error: new Error(resolution.error.message) }; } const executionStatus: OnlineEvalExecutionStatus = action === 'pause' ? 'DISABLED' : 'ENABLED'; @@ -106,6 +95,6 @@ export async function handlePauseResume( executionStatus: result.executionStatus, }; } catch (err) { - return { success: false, error: (err as Error).message }; + return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; } } diff --git a/src/cli/operations/eval/run-batch-evaluation.ts b/src/cli/operations/eval/run-batch-evaluation.ts index 0962f4e0a..2cc595bb6 100644 --- a/src/cli/operations/eval/run-batch-evaluation.ts +++ b/src/cli/operations/eval/run-batch-evaluation.ts @@ -7,6 +7,7 @@ * 5. Return results */ import { ConfigIO } from '../../../lib'; +import type { Result } from '../../../lib/types'; import type { DeployedState } from '../../../schema'; import { generateClientToken, getBatchEvaluation, startBatchEvaluation } from '../../aws/agentcore-batch-evaluation'; import type { @@ -54,18 +55,17 @@ export interface BatchEvaluationResult { error?: string; } -export interface RunBatchEvaluationCommandResult { - success: boolean; - error?: string; +export type RunBatchEvaluationCommandResult = Result<{ + evaluationResults?: EvaluationResults; +}> & { batchEvaluationId?: string; name?: string; status?: string; results: BatchEvaluationResult[]; - evaluationResults?: EvaluationResults; startedAt?: string; completedAt?: string; logFilePath?: string; -} +}; // ============================================================================ // Constants @@ -116,7 +116,7 @@ export async function runBatchEvaluationCommand( logger?.log(error, 'error'); logger?.endStep('error', error); logger?.finalize(false); - return { success: false, error, results: [], logFilePath: logger?.logFilePath }; + return { success: false, error: new Error(error), results: [], logFilePath: logger?.logFilePath }; } const runtimeId = agentState.runtimeId; @@ -152,7 +152,9 @@ export async function runBatchEvaluationCommand( if (!/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/.test(options.name)) { return { success: false, - error: `Batch evaluation name must start with a letter and contain only letters, digits, and underscores (max 48 chars). Got: "${options.name}"`, + error: new Error( + `Batch evaluation name must start with a letter and contain only letters, digits, and underscores (max 48 chars). Got: "${options.name}"` + ), results: [], logFilePath: logger?.logFilePath, }; @@ -242,7 +244,7 @@ export async function runBatchEvaluationCommand( logger?.finalize(false); return { success: false, - error, + error: new Error(error), batchEvaluationId: startResult.batchEvaluationId, name: evalName, status: current.status, @@ -287,7 +289,12 @@ export async function runBatchEvaluationCommand( const error = err instanceof Error ? err.message : String(err); logger?.log(error, 'error'); logger?.finalize(false); - return { success: false, error, results: [], logFilePath: logger?.logFilePath }; + return { + success: false, + error: err instanceof Error ? err : new Error(error), + results: [], + logFilePath: logger?.logFilePath, + }; } } diff --git a/src/cli/operations/eval/run-eval.ts b/src/cli/operations/eval/run-eval.ts index 90cd519c7..393068d9d 100644 --- a/src/cli/operations/eval/run-eval.ts +++ b/src/cli/operations/eval/run-eval.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { getCredentialProvider } from '../../aws'; import { evaluate } from '../../aws/agentcore'; import type { EvaluationReferenceInput } from '../../aws/agentcore'; @@ -30,7 +31,7 @@ interface ResolvedEvalContext { evaluatorLabels: string[]; } -type ResolveResult = { success: true; ctx: ResolvedEvalContext } | { success: false; error: string }; +type ResolveResult = Result<{ ctx: ResolvedEvalContext }>; /** * Resolve evaluator IDs from ARN strings or raw IDs. @@ -53,18 +54,18 @@ function resolveFromArn(options: RunEvalOptions): ResolveResult { // Parse ARN: arn:aws:bedrock-agentcore:::runtime/ const arnParts = arn.split(':'); if (arnParts.length < 6) { - return { success: false, error: `Invalid agent runtime ARN: ${arn}` }; + return { success: false, error: new Error(`Invalid agent runtime ARN: ${arn}`) }; } const region = options.region ?? arnParts[3]; if (!region) { - return { success: false, error: 'Could not determine region from ARN. Use --region to specify.' }; + return { success: false, error: new Error('Could not determine region from ARN. Use --region to specify.') }; } const resourcePart = arnParts.slice(5).join(':'); const runtimeMatch = /runtime\/(.+)$/.exec(resourcePart); if (!runtimeMatch) { - return { success: false, error: `Could not extract runtime ID from ARN: ${arn}` }; + return { success: false, error: new Error(`Could not extract runtime ID from ARN: ${arn}`) }; } const runtimeId = runtimeMatch[1]!; @@ -79,7 +80,9 @@ function resolveFromArn(options: RunEvalOptions): ResolveResult { } else { return { success: false, - error: `Custom evaluator "${evalName}" cannot be resolved in ARN mode. Use --evaluator-arn with an evaluator ARN or ID, or use Builtin.* evaluators.`, + error: new Error( + `Custom evaluator "${evalName}" cannot be resolved in ARN mode. Use --evaluator-arn with an evaluator ARN or ID, or use Builtin.* evaluators.` + ), }; } } @@ -91,7 +94,10 @@ function resolveFromArn(options: RunEvalOptions): ResolveResult { } if (evaluatorIds.length === 0) { - return { success: false, error: 'No evaluators specified. Use -e/--evaluator with Builtin.* or --evaluator-arn.' }; + return { + success: false, + error: new Error('No evaluators specified. Use -e/--evaluator with Builtin.* or --evaluator-arn.'), + }; } const endpointName = options.endpoint ?? process.env.AGENTCORE_RUNTIME_ENDPOINT ?? DEFAULT_ENDPOINT_NAME; @@ -139,7 +145,7 @@ function resolveFromProject(context: DeployedProjectConfig, options: RunEvalOpti if (!deployedEval) { return { success: false, - error: `Evaluator "${evalName}" not found in deployed state. Has it been deployed?`, + error: new Error(`Evaluator "${evalName}" not found in deployed state. Has it been deployed?`), }; } evaluatorIds.push(deployedEval.evaluatorId); @@ -154,7 +160,7 @@ function resolveFromProject(context: DeployedProjectConfig, options: RunEvalOpti } if (evaluatorIds.length === 0) { - return { success: false, error: 'No evaluators specified. Use -e/--evaluator or --evaluator-arn.' }; + return { success: false, error: new Error('No evaluators specified. Use -e/--evaluator or --evaluator-arn.') }; } return { @@ -551,12 +557,7 @@ async function fetchSessionSpans(opts: FetchSpansOptions): Promise; export async function handleRunEval(options: RunEvalOptions): Promise { let resolution: ResolveResult; @@ -569,7 +570,7 @@ export async function handleRunEval(options: RunEvalOptions): Promise { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('Gateway "dup-gw" already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('Gateway "dup-gw" already exists') }), + }) ); }); diff --git a/src/cli/operations/memory/__tests__/create-memory.test.ts b/src/cli/operations/memory/__tests__/create-memory.test.ts index a0b8077c4..09c827ac0 100644 --- a/src/cli/operations/memory/__tests__/create-memory.test.ts +++ b/src/cli/operations/memory/__tests__/create-memory.test.ts @@ -86,7 +86,7 @@ describe('add', () => { expiry: 30, }); - expect(result).toEqual(expect.objectContaining({ success: false, error: expect.any(String) })); + expect(result).toEqual(expect.objectContaining({ success: false, error: expect.any(Error) })); expect(mockWriteProjectSpec).not.toHaveBeenCalled(); }); @@ -96,7 +96,10 @@ describe('add', () => { const result = await primitive.add({ name: 'Existing', strategies: '', expiry: 30 }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('Memory "Existing" already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('Memory "Existing" already exists') }), + }) ); }); }); diff --git a/src/cli/operations/memory/list-memory-records.ts b/src/cli/operations/memory/list-memory-records.ts index 8bbf34628..b31b077d6 100644 --- a/src/cli/operations/memory/list-memory-records.ts +++ b/src/cli/operations/memory/list-memory-records.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { getCredentialProvider } from '../../aws'; import { BedrockAgentCoreClient, ListMemoryRecordsCommand } from '@aws-sdk/client-bedrock-agentcore'; @@ -20,12 +21,7 @@ export interface ListMemoryRecordsOptions { nextToken?: string; } -export interface ListMemoryRecordsResult { - success: boolean; - records?: MemoryRecordEntry[]; - nextToken?: string; - error?: string; -} +export type ListMemoryRecordsResult = Result<{ records: MemoryRecordEntry[]; nextToken?: string }>; /** * Lists memory records for a deployed memory resource via the AWS SDK. @@ -63,8 +59,8 @@ export async function listMemoryRecords(options: ListMemoryRecordsOptions): Prom } catch (error: unknown) { const err = error as Error; if (err.name === 'ResourceNotFoundException') { - return { success: false, error: `Memory '${memoryId}' not found. It may not have been deployed yet.` }; + return { success: false, error: new Error(`Memory '${memoryId}' not found. It may not have been deployed yet.`) }; } - return { success: false, error: err.message ?? String(error) }; + return { success: false, error: new Error(err.message ?? String(error)) }; } } diff --git a/src/cli/operations/memory/retrieve-memory-records.ts b/src/cli/operations/memory/retrieve-memory-records.ts index e8d2a65ed..d2414ef25 100644 --- a/src/cli/operations/memory/retrieve-memory-records.ts +++ b/src/cli/operations/memory/retrieve-memory-records.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { getCredentialProvider } from '../../aws'; import type { MemoryRecordEntry } from './list-memory-records'; import { BedrockAgentCoreClient, RetrieveMemoryRecordsCommand } from '@aws-sdk/client-bedrock-agentcore'; @@ -13,12 +14,7 @@ export interface RetrieveMemoryRecordsOptions { nextToken?: string; } -export interface RetrieveMemoryRecordsResult { - success: boolean; - records?: MemoryRecordEntry[]; - nextToken?: string; - error?: string; -} +export type RetrieveMemoryRecordsResult = Result<{ records: MemoryRecordEntry[]; nextToken?: string }>; /** * Searches memory records using semantic retrieval via the AWS SDK. @@ -62,8 +58,8 @@ export async function retrieveMemoryRecords( } catch (error: unknown) { const err = error as Error; if (err.name === 'ResourceNotFoundException') { - return { success: false, error: `Memory '${memoryId}' not found. It may not have been deployed yet.` }; + return { success: false, error: new Error(`Memory '${memoryId}' not found. It may not have been deployed yet.`) }; } - return { success: false, error: err.message ?? String(error) }; + return { success: false, error: new Error(err.message ?? String(error)) }; } } diff --git a/src/cli/operations/recommendation/__tests__/apply-to-bundle.test.ts b/src/cli/operations/recommendation/__tests__/apply-to-bundle.test.ts index 5e0fb668a..686d5daed 100644 --- a/src/cli/operations/recommendation/__tests__/apply-to-bundle.test.ts +++ b/src/cli/operations/recommendation/__tests__/apply-to-bundle.test.ts @@ -99,6 +99,7 @@ describe('applyRecommendationToBundle', () => { ); expect(applyResult.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(applyResult.newVersionId).toBe(NEW_VERSION_ID); // Verify spec was written with server components @@ -133,6 +134,7 @@ describe('applyRecommendationToBundle', () => { ); expect(applyResult.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(applyResult.newVersionId).toBe(NEW_VERSION_ID); }); @@ -153,6 +155,7 @@ describe('applyRecommendationToBundle', () => { ); expect(applyResult.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(applyResult.newVersionId).toBe(NEW_VERSION_ID); }); @@ -172,7 +175,8 @@ describe('applyRecommendationToBundle', () => { ); expect(applyResult.success).toBe(false); - expect(applyResult.error).toContain('does not contain a new config bundle version'); + // @ts-expect-error -- test accesses failure-branch field + expect(applyResult.error.message).toContain('does not contain a new config bundle version'); expect(writeSpecSpy).not.toHaveBeenCalled(); }); @@ -193,7 +197,8 @@ describe('applyRecommendationToBundle', () => { ); expect(applyResult.success).toBe(false); - expect(applyResult.error).toContain('NonExistent'); + // @ts-expect-error -- test accesses failure-branch field + expect(applyResult.error.message).toContain('NonExistent'); expect(writeSpecSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/cli/operations/recommendation/__tests__/recommendation-storage.test.ts b/src/cli/operations/recommendation/__tests__/recommendation-storage.test.ts index f6a60b6e8..1ff0f4c55 100644 --- a/src/cli/operations/recommendation/__tests__/recommendation-storage.test.ts +++ b/src/cli/operations/recommendation/__tests__/recommendation-storage.test.ts @@ -17,9 +17,9 @@ function makeTmpDir(): string { return dir; } -function makeResult(overrides: Partial = {}): RunRecommendationCommandResult { +function makeResult(overrides: Record = {}): RunRecommendationCommandResult { return { - success: true, + success: true as const, recommendationId: 'rec-123', status: 'COMPLETED', startedAt: '2026-03-24T10:00:00.000Z', diff --git a/src/cli/operations/recommendation/__tests__/run-recommendation.test.ts b/src/cli/operations/recommendation/__tests__/run-recommendation.test.ts index b26a59b32..7e7dfab9a 100644 --- a/src/cli/operations/recommendation/__tests__/run-recommendation.test.ts +++ b/src/cli/operations/recommendation/__tests__/run-recommendation.test.ts @@ -72,8 +72,10 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('NonExistentAgent'); - expect(result.error).toContain('not deployed'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('NonExistentAgent'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not deployed'); }); it('returns error when evaluator cannot be resolved', async () => { @@ -87,8 +89,10 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('UnknownEvaluator'); - expect(result.error).toContain('not found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('UnknownEvaluator'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not found'); }); it('returns result on COMPLETED status', async () => { @@ -126,6 +130,7 @@ describe('runRecommendationCommand', () => { expect(result.success).toBe(true); expect(result.recommendationId).toBe('rec-001'); expect(result.status).toBe('COMPLETED'); + // @ts-expect-error -- test accesses discriminated union field expect(result.result?.systemPromptRecommendationResult?.recommendedSystemPrompt).toBe('Optimized prompt'); }); @@ -154,7 +159,8 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('FAILED'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('FAILED'); expect(result.recommendationId).toBe('rec-002'); }); @@ -288,7 +294,8 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('API timeout'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('API timeout'); }); it('retries transient poll failures and succeeds', async () => { @@ -346,9 +353,12 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('consecutive errors'); - expect(result.error).toContain('fetch failed'); - expect(result.error).toContain('rec-retry-fail'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('consecutive errors'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('fetch failed'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('rec-retry-fail'); expect(mockGetRecommendation).toHaveBeenCalledTimes(3); }); @@ -378,8 +388,10 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Polling timed out'); - expect(result.error).toContain('rec-timeout'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Polling timed out'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('rec-timeout'); }); it('reads system prompt from file when inputSource is file', async () => { @@ -539,7 +551,8 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('No spans found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No spans found'); }); it('derives service name from runtimeId by stripping hash suffix', async () => { @@ -665,9 +678,12 @@ describe('runRecommendationCommand', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Insufficient trace data'); - expect(result.error).toContain('INSUFFICIENT_DATA'); - expect(result.error).toContain('Not enough traces'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Insufficient trace data'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('INSUFFICIENT_DATA'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Not enough traces'); // Request IDs are logged to file only, not included in the error message }); diff --git a/src/cli/operations/recommendation/apply-to-bundle.ts b/src/cli/operations/recommendation/apply-to-bundle.ts index bf9060d10..8ed55ff0d 100644 --- a/src/cli/operations/recommendation/apply-to-bundle.ts +++ b/src/cli/operations/recommendation/apply-to-bundle.ts @@ -10,6 +10,7 @@ * updates the local agentcore.json components to match the server state. */ import { ConfigIO } from '../../../lib'; +import type { Result } from '../../../lib/types'; import { getConfigurationBundleVersion } from '../../aws/agentcore-config-bundles'; import type { RecommendationResult } from '../../aws/agentcore-recommendation'; @@ -24,12 +25,7 @@ export interface ApplyRecommendationOptions { region: string; } -export interface ApplyRecommendationResult { - success: boolean; - error?: string; - /** New version ID that was synced from the server */ - newVersionId?: string; -} +export type ApplyRecommendationResult = Result<{ newVersionId: string }>; /** * Extract the bundleId from a bundle ARN. @@ -58,8 +54,9 @@ export async function applyRecommendationToBundle( if (!resultBundle) { return { success: false, - error: - 'Recommendation result does not contain a new config bundle version. The server may not have applied the recommendation to the bundle.', + error: new Error( + 'Recommendation result does not contain a new config bundle version. The server may not have applied the recommendation to the bundle.' + ), }; } @@ -67,7 +64,7 @@ export async function applyRecommendationToBundle( if (!bundleId) { return { success: false, - error: `Could not extract bundle ID from ARN: ${resultBundle.bundleArn}`, + error: new Error(`Could not extract bundle ID from ARN: ${resultBundle.bundleArn}`), }; } @@ -107,7 +104,7 @@ export async function applyRecommendationToBundle( if (!bundle) { return { success: false, - error: `Config bundle "${identifier}" not found in agentcore.json.`, + error: new Error(`Config bundle "${identifier}" not found in agentcore.json.`), }; } diff --git a/src/cli/operations/recommendation/recommendation-storage.ts b/src/cli/operations/recommendation/recommendation-storage.ts index e60846574..575a86ef9 100644 --- a/src/cli/operations/recommendation/recommendation-storage.ts +++ b/src/cli/operations/recommendation/recommendation-storage.ts @@ -45,7 +45,7 @@ export function saveRecommendationRun( status: result.status ?? 'unknown', startedAt: result.startedAt, completedAt: result.completedAt, - result: result.result, + result: result.success ? result.result : undefined, }; writeFileSync(filePath, JSON.stringify(record, null, 2)); diff --git a/src/cli/operations/recommendation/run-recommendation.ts b/src/cli/operations/recommendation/run-recommendation.ts index 0423cfe32..25ab9a1d3 100644 --- a/src/cli/operations/recommendation/run-recommendation.ts +++ b/src/cli/operations/recommendation/run-recommendation.ts @@ -60,7 +60,7 @@ export async function runRecommendationCommand( logger?.finalize(false); return { success: false, - error: `Agent "${options.agent}" not deployed. Run \`agentcore deploy\` first.`, + error: new Error(`Agent "${options.agent}" not deployed. Run \`agentcore deploy\` first.`), logFilePath: logger?.logFilePath, }; } @@ -73,7 +73,9 @@ export async function runRecommendationCommand( if (!evaluatorId) { return { success: false, - error: `Evaluator "${evaluator}" not found in deployed state. Use a Builtin.* name, a full ARN, or deploy a custom evaluator first.`, + error: new Error( + `Evaluator "${evaluator}" not found in deployed state. Use a Builtin.* name, a full ARN, or deploy a custom evaluator first.` + ), logFilePath: logger?.logFilePath, }; } @@ -82,7 +84,7 @@ export async function runRecommendationCommand( if (options.type === 'SYSTEM_PROMPT_RECOMMENDATION' && evaluatorIds.length !== 1) { return { success: false, - error: 'System prompt recommendations require exactly one evaluator.', + error: new Error('System prompt recommendations require exactly one evaluator.'), logFilePath: logger?.logFilePath, }; } @@ -105,7 +107,7 @@ export async function runRecommendationCommand( ) { return { success: false, - error: 'System prompt content is required. Provide via --inline, --prompt-file, or --bundle-name.', + error: new Error('System prompt content is required. Provide via --inline, --prompt-file, or --bundle-name.'), logFilePath: logger?.logFilePath, }; } @@ -132,7 +134,9 @@ export async function runRecommendationCommand( if (!bundleArn) { return { success: false, - error: `Config bundle "${options.bundleName}" not found in deployed state. Run \`agentcore deploy\` first.`, + error: new Error( + `Config bundle "${options.bundleName}" not found in deployed state. Run \`agentcore deploy\` first.` + ), logFilePath: logger?.logFilePath, }; } @@ -230,7 +234,9 @@ export async function runRecommendationCommand( logger?.finalize(false); return { success: false, - error: `Polling timed out after ${Math.round(maxDurationMs / 60000)} minutes. The recommendation may still be running server-side.\nRecommendation ID: ${startResult.recommendationId}`, + error: new Error( + `Polling timed out after ${Math.round(maxDurationMs / 60000)} minutes. The recommendation may still be running server-side.\nRecommendation ID: ${startResult.recommendationId}` + ), recommendationId: startResult.recommendationId, status: currentStatus, logFilePath: logger?.logFilePath, @@ -255,7 +261,9 @@ export async function runRecommendationCommand( logger?.finalize(false); return { success: false, - error: `Polling failed after ${MAX_POLL_RETRIES} consecutive errors: ${pollErrMsg}\nThe recommendation may still be running server-side.\nRecommendation ID: ${startResult.recommendationId}`, + error: new Error( + `Polling failed after ${MAX_POLL_RETRIES} consecutive errors: ${pollErrMsg}\nThe recommendation may still be running server-side.\nRecommendation ID: ${startResult.recommendationId}` + ), recommendationId: startResult.recommendationId, status: currentStatus, logFilePath: logger?.logFilePath, @@ -303,9 +311,11 @@ export async function runRecommendationCommand( return { success: false, - error: failureDetails - ? `Recommendation failed: ${failureDetails}` - : `Recommendation finished with status: ${currentStatus}`, + error: new Error( + failureDetails + ? `Recommendation failed: ${failureDetails}` + : `Recommendation finished with status: ${currentStatus}` + ), recommendationId: startResult.recommendationId, status: currentStatus, logFilePath: logger?.logFilePath, @@ -319,7 +329,7 @@ export async function runRecommendationCommand( logger?.finalize(false); return { success: false, - error: `Recommendation ended with unexpected status: ${currentStatus}`, + error: new Error(`Recommendation ended with unexpected status: ${currentStatus}`), recommendationId: startResult.recommendationId, status: currentStatus, logFilePath: logger?.logFilePath, @@ -331,7 +341,7 @@ export async function runRecommendationCommand( logger?.finalize(false); return { success: false, - error: errorMsg, + error: err instanceof Error ? err : new Error(errorMsg), logFilePath: logger?.logFilePath, }; } diff --git a/src/cli/operations/recommendation/types.ts b/src/cli/operations/recommendation/types.ts index 426ba84a8..74393a33b 100644 --- a/src/cli/operations/recommendation/types.ts +++ b/src/cli/operations/recommendation/types.ts @@ -1,6 +1,7 @@ /** * Shared types for the recommendation feature. */ +import type { Result } from '../../../lib/types'; import type { RecommendationResult, RecommendationType } from '../../aws/agentcore-recommendation'; export type { RecommendationType } from '../../aws/agentcore-recommendation'; @@ -56,17 +57,13 @@ export interface RunRecommendationCommandOptions { onStarted?: (info: { recommendationId: string; region: string }) => void; } -export interface RunRecommendationCommandResult { - success: boolean; - error?: string; - recommendationId?: string; - status?: string; - /** The recommendation result from the API (populated on COMPLETED) */ +export type RunRecommendationCommandResult = Result<{ result?: RecommendationResult; - /** Resolved AWS region used for the recommendation */ region?: string; +}> & { + recommendationId?: string; + status?: string; startedAt?: string; completedAt?: string; - /** Path to the execution log file */ logFilePath?: string; -} +}; diff --git a/src/cli/operations/remove/__tests__/remove-agent-ops.test.ts b/src/cli/operations/remove/__tests__/remove-agent-ops.test.ts index fb2b9fedc..ee955342c 100644 --- a/src/cli/operations/remove/__tests__/remove-agent-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-agent-ops.test.ts @@ -87,7 +87,10 @@ describe('remove', () => { const result = await primitive.remove('Missing'); - expect(result).toEqual({ success: false, error: 'Agent "Missing" not found.' }); + expect(result).toEqual({ + success: false, + error: expect.objectContaining({ message: 'Agent "Missing" not found.' }), + }); }); it('returns error on exception', async () => { @@ -95,6 +98,6 @@ describe('remove', () => { const result = await primitive.remove('Agent1'); - expect(result).toEqual({ success: false, error: 'read fail' }); + expect(result).toEqual({ success: false, error: expect.objectContaining({ message: 'read fail' }) }); }); }); diff --git a/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts b/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts index 54293df5a..e06aa4f78 100644 --- a/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts @@ -101,7 +101,10 @@ describe('remove', () => { const result = await primitive.remove('missing'); - expect(result).toEqual({ success: false, error: 'Gateway "missing" not found.' }); + expect(result).toEqual({ + success: false, + error: expect.objectContaining({ message: 'Gateway "missing" not found.' }), + }); }); it('returns error on exception', async () => { @@ -109,6 +112,6 @@ describe('remove', () => { const result = await primitive.remove('gw1'); - expect(result).toEqual({ success: false, error: 'read fail' }); + expect(result).toEqual({ success: false, error: expect.objectContaining({ message: 'read fail' }) }); }); }); diff --git a/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts b/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts index 1b1bbb8e7..6610b2950 100644 --- a/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts @@ -93,7 +93,10 @@ describe('remove', () => { const result = await primitive.remove('Missing'); - expect(result).toEqual({ success: false, error: 'Credential "Missing" not found.' }); + expect(result).toEqual({ + success: false, + error: expect.objectContaining({ message: 'Credential "Missing" not found.' }), + }); }); it('returns error on exception', async () => { @@ -101,6 +104,6 @@ describe('remove', () => { const result = await primitive.remove('Cred1'); - expect(result).toEqual({ success: false, error: 'read fail' }); + expect(result).toEqual({ success: false, error: expect.objectContaining({ message: 'read fail' }) }); }); }); diff --git a/src/cli/operations/remove/__tests__/remove-memory-ops.test.ts b/src/cli/operations/remove/__tests__/remove-memory-ops.test.ts index d42bedb94..0ef487340 100644 --- a/src/cli/operations/remove/__tests__/remove-memory-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-memory-ops.test.ts @@ -84,7 +84,10 @@ describe('remove', () => { const result = await primitive.remove('Missing'); - expect(result).toEqual({ success: false, error: 'Memory "Missing" not found.' }); + expect(result).toEqual({ + success: false, + error: expect.objectContaining({ message: 'Memory "Missing" not found.' }), + }); }); it('returns error on exception', async () => { @@ -92,6 +95,6 @@ describe('remove', () => { const result = await primitive.remove('Mem1'); - expect(result).toEqual({ success: false, error: 'read fail' }); + expect(result).toEqual({ success: false, error: expect.objectContaining({ message: 'read fail' }) }); }); }); diff --git a/src/cli/operations/remove/remove-gateway-target.ts b/src/cli/operations/remove/remove-gateway-target.ts index 0ceebf7eb..5a1f7e2d8 100644 --- a/src/cli/operations/remove/remove-gateway-target.ts +++ b/src/cli/operations/remove/remove-gateway-target.ts @@ -1,6 +1,7 @@ import { ConfigIO } from '../../../lib'; import type { AgentCoreCliMcpDefs, AgentCoreMcpSpec } from '../../../schema'; -import type { RemovalPreview, RemovalResult, SchemaChange } from './types'; +import { getErrorMessage } from '../../errors'; +import type { RemovalPreview, Result, SchemaChange } from './types'; import { existsSync } from 'fs'; import { rm } from 'fs/promises'; import { join } from 'path'; @@ -155,7 +156,7 @@ function computeRemovedToolMcpDefs( /** * Remove a gateway target from the project. */ -export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise { +export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise { try { const configIO = new ConfigIO(); const project = await configIO.readProjectSpec(); @@ -172,11 +173,11 @@ export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise const gateway = mcpSpec.agentCoreGateways.find(g => g.name === tool.gatewayName); if (!gateway) { - return { success: false, error: `Gateway "${tool.gatewayName}" not found.` }; + return { success: false, error: new Error(`Gateway "${tool.gatewayName}" not found.`) }; } const target = gateway.targets.find(t => t.name === tool.name); if (!target) { - return { success: false, error: `Target "${tool.name}" not found in gateway "${tool.gatewayName}".` }; + return { success: false, error: new Error(`Target "${tool.name}" not found in gateway "${tool.gatewayName}".`) }; } if (target.compute?.implementation && 'path' in target.compute.implementation) { toolPath = target.compute.implementation.path; @@ -200,7 +201,6 @@ export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise return { success: true }; } catch (err) { - const message = err instanceof Error ? err.message : 'Unknown error'; - return { success: false, error: message }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } diff --git a/src/cli/operations/remove/types.ts b/src/cli/operations/remove/types.ts index 2194a66f7..18a2c565d 100644 --- a/src/cli/operations/remove/types.ts +++ b/src/cli/operations/remove/types.ts @@ -21,10 +21,7 @@ export interface RemovalPreview { schemaChanges: SchemaChange[]; } -/** - * Result of a removal operation. - */ -export type RemovalResult = { success: true } | { success: false; error: string }; +export type { Result } from '../../../lib/types'; /** * Snapshot of all schemas before removal (for diff computation). diff --git a/src/cli/operations/resolve-agent.ts b/src/cli/operations/resolve-agent.ts index 8f8ee6ba3..165ad23d1 100644 --- a/src/cli/operations/resolve-agent.ts +++ b/src/cli/operations/resolve-agent.ts @@ -1,4 +1,5 @@ import { ConfigIO } from '../../lib'; +import type { Result } from '../../lib/types'; import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../schema'; export interface DeployedProjectConfig { @@ -32,11 +33,11 @@ export async function loadDeployedProjectConfig(configIO: ConfigIO = new ConfigI export function resolveAgent( context: DeployedProjectConfig, options: { runtime?: string } -): { success: true; agent: ResolvedAgent } | { success: false; error: string } { +): Result<{ agent: ResolvedAgent }> { const { project, deployedState, awsTargets } = context; if (project.runtimes.length === 0) { - return { success: false, error: 'No runtimes defined in agentcore.json' }; + return { success: false, error: new Error('No runtimes defined in agentcore.json') }; } // Resolve runtime @@ -45,7 +46,7 @@ export function resolveAgent( if (!options.runtime && project.runtimes.length > 1) { return { success: false, - error: `Multiple runtimes found. Use --runtime to specify one: ${runtimeNames.join(', ')}`, + error: new Error(`Multiple runtimes found. Use --runtime to specify one: ${runtimeNames.join(', ')}`), }; } @@ -54,18 +55,18 @@ export function resolveAgent( if (options.runtime && !agentSpec) { return { success: false, - error: `Runtime '${options.runtime}' not found. Available: ${runtimeNames.join(', ')}`, + error: new Error(`Runtime '${options.runtime}' not found. Available: ${runtimeNames.join(', ')}`), }; } if (!agentSpec) { - return { success: false, error: 'No runtimes defined in agentcore.json' }; + return { success: false, error: new Error('No runtimes defined in agentcore.json') }; } // Resolve target const targetNames = Object.keys(deployedState.targets); if (targetNames.length === 0) { - return { success: false, error: 'No deployed targets found. Run `agentcore deploy` first.' }; + return { success: false, error: new Error('No deployed targets found. Run `agentcore deploy` first.') }; } const selectedTargetName = targetNames[0]!; @@ -73,7 +74,7 @@ export function resolveAgent( const targetConfig = awsTargets.find(t => t.name === selectedTargetName); if (!targetConfig) { - return { success: false, error: `Target config '${selectedTargetName}' not found in aws-targets` }; + return { success: false, error: new Error(`Target config '${selectedTargetName}' not found in aws-targets`) }; } // Get the deployed state for this specific agent @@ -82,7 +83,9 @@ export function resolveAgent( if (!agentState) { return { success: false, - error: `Runtime '${agentSpec.name}' is not deployed to target '${selectedTargetName}'. Run 'agentcore deploy' first.`, + error: new Error( + `Runtime '${agentSpec.name}' is not deployed to target '${selectedTargetName}'. Run 'agentcore deploy' first.` + ), }; } diff --git a/src/cli/operations/traces/__tests__/get-trace.test.ts b/src/cli/operations/traces/__tests__/get-trace.test.ts index c6fda22f4..6611389f0 100644 --- a/src/cli/operations/traces/__tests__/get-trace.test.ts +++ b/src/cli/operations/traces/__tests__/get-trace.test.ts @@ -62,12 +62,15 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.records).toHaveLength(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.records![0]).toEqual({ '@timestamp': '2024-01-01T00:00:00Z', '@message': { traceId: 'abc123', spanId: 'span1' }, '@ptr': 'ptr-value-1', }); + // @ts-expect-error -- test accesses discriminated union field expect(result.records![1]).toEqual({ '@timestamp': '2024-01-01T00:00:01Z', '@message': { traceId: 'abc123', spanId: 'span2' }, @@ -81,7 +84,8 @@ describe('fetchTraceRecords', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid trace ID format'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Invalid trace ID format'); expect(mockSend).not.toHaveBeenCalled(); }); @@ -94,7 +98,8 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(false); - expect(result.error).toContain('No trace data found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('No trace data found'); }); it('returns error when query fails to start', async () => { @@ -103,7 +108,8 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(false); - expect(result.error).toContain('Failed to start CloudWatch Logs Insights query'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Failed to start CloudWatch Logs Insights query'); }); it('returns error when query status is Failed', async () => { @@ -112,7 +118,8 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(false); - expect(result.error).toContain('failed'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('failed'); }); it('preserves @ptr when present in CloudWatch response', async () => { @@ -130,7 +137,9 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.records).toHaveLength(1); + // @ts-expect-error -- test accesses discriminated union field expect(result.records![0]!['@ptr']).toBe('cw-ptr-123'); }); @@ -148,6 +157,7 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field expect(result.records![0]).not.toHaveProperty('@ptr'); }); @@ -165,7 +175,9 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.records).toHaveLength(1); + // @ts-expect-error -- test accesses discriminated union field expect(result.records![0]!['@message']).toBe('plain text message'); }); @@ -177,8 +189,10 @@ describe('fetchTraceRecords', () => { const result = await fetchTraceRecords(baseOptions); expect(result.success).toBe(false); - expect(result.error).toContain('Log group'); - expect(result.error).toContain('not found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Log group'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('not found'); }); }); @@ -209,6 +223,7 @@ describe('getTrace', () => { }); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.filePath).toContain('test-trace.json'); expect(fs.default.mkdirSync).toHaveBeenCalled(); expect(fs.default.writeFileSync).toHaveBeenCalledWith('/tmp/test-trace.json', expect.stringContaining('"traceId"')); @@ -227,7 +242,8 @@ describe('getTrace', () => { }); expect(result.success).toBe(false); - expect(result.error).toContain('Invalid trace ID format'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toContain('Invalid trace ID format'); expect(fs.default.writeFileSync).not.toHaveBeenCalled(); }); }); diff --git a/src/cli/operations/traces/__tests__/list-traces.test.ts b/src/cli/operations/traces/__tests__/list-traces.test.ts index 0bbe884de..a5513f439 100644 --- a/src/cli/operations/traces/__tests__/list-traces.test.ts +++ b/src/cli/operations/traces/__tests__/list-traces.test.ts @@ -39,13 +39,16 @@ describe('listTraces', () => { const result = await listTraces(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.traces).toHaveLength(2); + // @ts-expect-error -- test accesses discriminated union field expect(result.traces![0]).toEqual({ traceId: 'trace-1', timestamp: '2024-01-01T00:05:00Z', sessionId: 'sess-1', spanCount: '12', }); + // @ts-expect-error -- test accesses discriminated union field expect(result.traces![1]).toEqual({ traceId: 'trace-2', timestamp: '2024-01-01T00:03:00Z', @@ -67,7 +70,9 @@ describe('listTraces', () => { const result = await listTraces(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.traces).toHaveLength(1); + // @ts-expect-error -- test accesses discriminated union field expect(result.traces![0]!.traceId).toBe('trace-1'); }); @@ -80,6 +85,7 @@ describe('listTraces', () => { const result = await listTraces(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses discriminated union field expect(result.traces![0]!.timestamp).toBe('2024-01-01T00:00:00Z'); }); @@ -92,19 +98,21 @@ describe('listTraces', () => { const result = await listTraces(baseOptions); expect(result.success).toBe(true); + // @ts-expect-error -- test accesses success-branch field expect(result.traces).toHaveLength(0); }); it('propagates errors from runInsightsQuery', async () => { mockRunInsightsQuery.mockResolvedValueOnce({ success: false, - error: 'Log group not found', + error: new Error('Log group not found'), }); const result = await listTraces(baseOptions); expect(result.success).toBe(false); - expect(result.error).toBe('Log group not found'); + // @ts-expect-error -- test accesses failure-branch field + expect(result.error.message).toBe('Log group not found'); }); it('passes correct log group name and default limit', async () => { diff --git a/src/cli/operations/traces/get-trace.ts b/src/cli/operations/traces/get-trace.ts index a87f10a65..1de18e426 100644 --- a/src/cli/operations/traces/get-trace.ts +++ b/src/cli/operations/traces/get-trace.ts @@ -23,9 +23,9 @@ async function fetchSpans( traceId: string, startTime?: number, endTime?: number -): Promise<{ success: boolean; spans?: CloudWatchSpanRecord[]; error?: string }> { +): Promise<{ success: boolean; spans?: CloudWatchSpanRecord[]; error?: Error }> { if (!TRACE_ID_PATTERN.test(traceId)) { - return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + return { success: false, error: new Error('Invalid trace ID format. Expected a hex string (e.g., abc123def456).') }; } const result = await runInsightsQuery({ @@ -82,7 +82,7 @@ export async function fetchTraceRecords(options: FetchTraceRecordsOptions): Prom const { region, runtimeId, traceId, includeSpans } = options; if (!TRACE_ID_PATTERN.test(traceId)) { - return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + return { success: false, error: new Error('Invalid trace ID format. Expected a hex string (e.g., abc123def456).') }; } const [recordsResult, spansResult] = await Promise.all([ @@ -106,7 +106,7 @@ export async function fetchTraceRecords(options: FetchTraceRecordsOptions): Prom const traceData = recordsResult.rows ?? []; if (traceData.length === 0 && (!spansResult || (spansResult.spans ?? []).length === 0)) { - return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + return { success: false, error: new Error(`No trace data found for trace ID: ${traceId}`) }; } const records: CloudWatchTraceRecord[] = traceData.map(entry => { @@ -146,7 +146,7 @@ export async function getTrace(options: GetTraceOptions): Promise { diff --git a/src/cli/operations/traces/insights-query.ts b/src/cli/operations/traces/insights-query.ts index 5a4da2031..c77465e97 100644 --- a/src/cli/operations/traces/insights-query.ts +++ b/src/cli/operations/traces/insights-query.ts @@ -1,3 +1,4 @@ +import type { Result } from '../../../lib/types'; import { getCredentialProvider } from '../../aws'; import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; @@ -11,11 +12,7 @@ export interface InsightsQueryOptions { endTime?: number; } -export interface InsightsQueryResult { - success: boolean; - rows?: Record[]; - error?: string; -} +export type InsightsQueryResult = Result<{ rows: Record[] }>; async function pollQueryResults(client: CloudWatchLogsClient, queryId: string): Promise { for (let i = 0; i < 60; i++) { @@ -26,7 +23,7 @@ async function pollQueryResults(client: CloudWatchLogsClient, queryId: string): if (status === 'Complete' || status === 'Failed' || status === 'Cancelled') { if (status !== 'Complete') { - return { success: false, error: `Query ${status.toLowerCase()}` }; + return { success: false, error: new Error(`Query ${status.toLowerCase()}`) }; } const rows = (queryResults.results ?? []).map(row => { @@ -42,7 +39,7 @@ async function pollQueryResults(client: CloudWatchLogsClient, queryId: string): } } - return { success: false, error: 'Query timed out after 60 seconds' }; + return { success: false, error: new Error('Query timed out after 60 seconds') }; } export async function runInsightsQuery(options: InsightsQueryOptions): Promise { @@ -68,7 +65,7 @@ export async function runInsightsQuery(options: InsightsQueryOptions): Promise; export interface GetTraceOptions { region: string; @@ -48,11 +45,7 @@ export interface GetTraceOptions { endTime?: number; } -export interface GetTraceResult { - success: boolean; - filePath?: string; - error?: string; -} +export type GetTraceResult = Result<{ filePath: string }>; export interface TraceEntry { traceId: string; @@ -70,8 +63,4 @@ export interface ListTracesOptions { endTime?: number; } -export interface ListTracesResult { - success: boolean; - traces?: TraceEntry[]; - error?: string; -} +export type ListTracesResult = Result<{ traces: TraceEntry[] }>; diff --git a/src/cli/primitives/ABTestPrimitive.ts b/src/cli/primitives/ABTestPrimitive.ts index 9dd973571..4f0db08b8 100644 --- a/src/cli/primitives/ABTestPrimitive.ts +++ b/src/cli/primitives/ABTestPrimitive.ts @@ -2,10 +2,10 @@ import { findConfigRoot } from '../../lib'; import type { ABTest } from '../../schema/schemas/primitives/ab-test'; import { ABTestSchema } from '../../schema/schemas/primitives/ab-test'; import { getErrorMessage } from '../errors'; -import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import type { RemovalPreview, Result, SchemaChange } from '../operations/remove/types'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; -import type { AddResult, AddScreenComponent, RemovableResource } from './types'; +import type { AddScreenComponent, RemovableResource } from './types'; import type { Command } from '@commander-js/extra-typings'; export type GatewayChoice = { type: 'create-new' } | { type: 'existing-http'; name: string }; @@ -60,22 +60,22 @@ export class ABTestPrimitive extends BasePrimitive> { + async add(options: AddABTestOptions): Promise> { try { const abTest = await this.createABTest(options); return { success: true, abTestName: abTest.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(testName: string, options?: { deleteGateway?: boolean }): Promise { + async remove(testName: string, options?: { deleteGateway?: boolean }): Promise { try { const project = await this.readProjectSpec(); const index = (project.abTests ?? []).findIndex(t => t.name === testName); if (index === -1) { - return { success: false, error: `AB test "${testName}" not found.` }; + return { success: false, error: new Error(`AB test "${testName}" not found.`) }; } const removedTest = project.abTests[index]!; @@ -123,7 +123,7 @@ export class ABTestPrimitive extends BasePrimitive> { + async addTargetBased(options: AddTargetBasedABTestOptions): Promise> { try { const abTest = await this.createTargetBasedABTest(options); return { success: true, abTestName: abTest.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index b9873990b..528af65ca 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -34,7 +34,7 @@ import { } from '../operations/agent/generate'; import { executeImportAgent } from '../operations/agent/import'; import { setupPythonProject } from '../operations/python'; -import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import type { RemovalPreview, Result, SchemaChange } from '../operations/remove/types'; import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { AgentType, @@ -55,7 +55,7 @@ import { BasePrimitive } from './BasePrimitive'; import { CredentialPrimitive } from './CredentialPrimitive'; import { buildAuthorizerConfigFromJwtConfig, createManagedOAuthCredential } from './auth-utils'; import { computeDefaultCredentialEnvVarName } from './credential-utils'; -import type { AddResult, AddScreenComponent, RemovableResource } from './types'; +import type { AddScreenComponent, RemovableResource } from './types'; import type { Command } from '@commander-js/extra-typings'; import { mkdirSync } from 'fs'; import { dirname, join } from 'path'; @@ -115,17 +115,17 @@ export class AgentPrimitive extends BasePrimitive> { + async add(options: AddAgentOptions): Promise> { try { const configBaseDir = findConfigRoot(); if (!configBaseDir) { - return { success: false, error: new NoProjectError().message }; + return { success: false, error: new NoProjectError() }; } const configIO = new ConfigIO({ baseDir: configBaseDir }); if (!configIO.configExists('project')) { - return { success: false, error: new NoProjectError().message }; + return { success: false, error: new NoProjectError() }; } const project = await configIO.readProjectSpec(); @@ -133,7 +133,9 @@ export class AgentPrimitive extends BasePrimitive { + async remove(agentName: string): Promise { try { const project = await this.readProjectSpec(); const agentIndex = project.runtimes.findIndex(a => a.name === agentName); if (agentIndex === -1) { - return { success: false, error: `Agent "${agentName}" not found.` }; + return { success: false, error: new Error(`Agent "${agentName}" not found.`) }; } // Remove agent (credentials preserved for potential reuse) @@ -164,8 +166,7 @@ export class AgentPrimitive extends BasePrimitive> { + ): Promise> { const projectRoot = dirname(configBaseDir); const configIO = new ConfigIO({ baseDir: configBaseDir }); const project = await configIO.readProjectSpec(); @@ -510,7 +511,7 @@ export class AgentPrimitive extends BasePrimitive> { + ): Promise> { return executeImportAgent({ name: options.name, framework: options.framework, @@ -532,7 +533,7 @@ export class AgentPrimitive extends BasePrimitive> { + ): Promise> { const codeLocation = options.codeLocation!.endsWith('/') ? options.codeLocation! : `${options.codeLocation!}/`; // Create the agent code directory so users know where to put their code diff --git a/src/cli/primitives/BasePrimitive.ts b/src/cli/primitives/BasePrimitive.ts index 149611c4f..1c689e491 100644 --- a/src/cli/primitives/BasePrimitive.ts +++ b/src/cli/primitives/BasePrimitive.ts @@ -4,7 +4,7 @@ import type { ResourceType } from '../commands/remove/types'; import { getErrorMessage } from '../errors'; import { requireTTY } from '../tui/guards/tty'; import { SOURCE_CODE_NOTE } from './constants'; -import type { AddResult, AddScreenComponent, RemovableResource, RemovalPreview, RemovalResult } from './types'; +import type { AddScreenComponent, RemovableResource, RemovalPreview, Result } from './types'; import type { Command } from '@commander-js/extra-typings'; import type { z } from 'zod'; @@ -37,12 +37,12 @@ export abstract class BasePrimitive< * Add a new resource of this type. * Each primitive owns its implementation entirely. */ - abstract add(options: TAddOptions): Promise; + abstract add(options: TAddOptions): Promise; /** * Remove a resource by name. */ - abstract remove(name: string): Promise; + abstract remove(name: string): Promise; /** * Preview what will be removed. @@ -128,7 +128,7 @@ export abstract class BasePrimitive< resourceName: cliOptions.name, message: result.success ? `Removed ${this.label.toLowerCase()} '${cliOptions.name}'` : undefined, note: result.success ? SOURCE_CODE_NOTE : undefined, - error: !result.success ? result.error : undefined, + error: !result.success ? result.error.message : undefined, }) ); process.exit(result.success ? 0 : 1); diff --git a/src/cli/primitives/ConfigBundlePrimitive.ts b/src/cli/primitives/ConfigBundlePrimitive.ts index 77f26205b..b10fb1dc1 100644 --- a/src/cli/primitives/ConfigBundlePrimitive.ts +++ b/src/cli/primitives/ConfigBundlePrimitive.ts @@ -2,9 +2,9 @@ import { findConfigRoot } from '../../lib'; import type { ConfigBundle } from '../../schema'; import { ConfigBundleSchema } from '../../schema'; import { getErrorMessage } from '../errors'; -import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import type { RemovalPreview, Result, SchemaChange } from '../operations/remove/types'; import { BasePrimitive } from './BasePrimitive'; -import type { AddResult, AddScreenComponent, RemovableResource } from './types'; +import type { AddScreenComponent, RemovableResource } from './types'; import type { Command } from '@commander-js/extra-typings'; import { readFileSync } from 'fs'; @@ -32,22 +32,22 @@ export class ConfigBundlePrimitive extends BasePrimitive> { + async add(options: AddConfigBundleOptions): Promise> { try { const bundle = await this.createConfigBundle(options); return { success: true, bundleName: bundle.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(bundleName: string): Promise { + async remove(bundleName: string): Promise { try { const project = await this.readProjectSpec(); const index = (project.configBundles ?? []).findIndex(b => b.name === bundleName); if (index === -1) { - return { success: false, error: `Configuration bundle "${bundleName}" not found.` }; + return { success: false, error: new Error(`Configuration bundle "${bundleName}" not found.`) }; } project.configBundles.splice(index, 1); @@ -55,7 +55,7 @@ export class ConfigBundlePrimitive extends BasePrimitive> { + async add(options: AddCredentialOptions): Promise> { try { const credential = await this.createCredential(options); return { success: true, credentialName: credential.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(credentialName: string, options?: { force?: boolean }): Promise { + async remove(credentialName: string, options?: { force?: boolean }): Promise { try { const project = await this.readProjectSpec(); const credentialIndex = project.credentials.findIndex(c => c.name === credentialName); if (credentialIndex === -1) { - return { success: false, error: `Credential "${credentialName}" not found.` }; + return { success: false, error: new Error(`Credential "${credentialName}" not found.`) }; } const credential = project.credentials[credentialIndex]!; @@ -96,7 +96,9 @@ export class CredentialPrimitive extends BasePrimitive t.name).join(', '); return { success: false, - error: `Credential "${credentialName}" is referenced by gateway target(s): ${targetList}. Use force to override.`, + error: new Error( + `Credential "${credentialName}" is referenced by gateway target(s): ${targetList}. Use force to override.` + ), }; } @@ -115,8 +119,7 @@ export class CredentialPrimitive extends BasePrimitive> { + async add(options: AddEvaluatorOptions): Promise> { try { const evaluator = await this.createEvaluator(options); @@ -57,17 +57,17 @@ export class EvaluatorPrimitive extends BasePrimitive { + async remove(evaluatorName: string): Promise { try { const project = await this.readProjectSpec(); const index = project.evaluators.findIndex(e => e.name === evaluatorName); if (index === -1) { - return { success: false, error: `Evaluator "${evaluatorName}" not found.` }; + return { success: false, error: new Error(`Evaluator "${evaluatorName}" not found.`) }; } // Warn if referenced by online eval configs @@ -76,7 +76,9 @@ export class EvaluatorPrimitive extends BasePrimitive c.name).join(', '); return { success: false, - error: `Evaluator "${evaluatorName}" is referenced by online eval config(s): ${configNames}. Remove those references first.`, + error: new Error( + `Evaluator "${evaluatorName}" is referenced by online eval config(s): ${configNames}. Remove those references first.` + ), }; } @@ -96,7 +98,7 @@ export class EvaluatorPrimitive extends BasePrimitive `{${p}}`).join(', '); + const placeholders = LEVEL_PLACEHOLDERS[level].map((p: string) => `{${p}}`).join(', '); fail( `--instructions is required in non-interactive mode (or use --config). ` + `Must include at least one placeholder for ${level}: ${placeholders}` @@ -296,7 +298,7 @@ export class EvaluatorPrimitive extends BasePrimitive> { + async add(options: AddGatewayOptions): Promise> { try { const config = this.buildGatewayConfig(options); const result = await this.createGatewayFromWizard(config); return { success: true, gatewayName: result.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(gatewayName: string): Promise { + async remove(gatewayName: string): Promise { try { const project = await this.readProjectSpec(); const mcpSpec = extractMcpSpec(project); const gateway = mcpSpec.agentCoreGateways.find(g => g.name === gatewayName); if (!gateway) { - return { success: false, error: `Gateway "${gatewayName}" not found.` }; + return { success: false, error: new Error(`Gateway "${gatewayName}" not found.`) }; } const newMcpSpec = this.computeRemovedGatewayMcpSpec(mcpSpec, gatewayName); @@ -86,8 +86,7 @@ export class GatewayPrimitive extends BasePrimitive> { + async add(options: AddGatewayTargetOptions): Promise> { try { const config = this.buildGatewayTargetConfig(options); const result = await this.createToolFromWizard(config); return { success: true, toolName: result.toolName, sourcePath: result.projectPath }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(name: string): Promise { + async remove(name: string): Promise { // Find the target by name to get its gateway info const tools = await this.getRemovable(); const tool = tools.find(t => t.name === name); if (!tool) { - return { success: false, error: `Gateway target "${name}" not found.` }; + return { success: false, error: new Error(`Gateway target "${name}" not found.`) }; } return this.removeGatewayTarget(tool); } @@ -183,7 +183,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { + async removeGatewayTarget(tool: RemovableGatewayTarget): Promise { try { const project = await this.readProjectSpec(); const mcpSpec = extractMcpSpec(project); @@ -195,11 +195,14 @@ export class GatewayTargetPrimitive extends BasePrimitive g.name === tool.gatewayName); if (!gateway) { - return { success: false, error: `Gateway "${tool.gatewayName}" not found.` }; + return { success: false, error: new Error(`Gateway "${tool.gatewayName}" not found.`) }; } const target = gateway.targets.find(t => t.name === tool.name); if (!target) { - return { success: false, error: `Target "${tool.name}" not found in gateway "${tool.gatewayName}".` }; + return { + success: false, + error: new Error(`Target "${tool.name}" not found in gateway "${tool.gatewayName}".`), + }; } if (target.compute?.implementation && 'path' in target.compute.implementation) { toolPath = target.compute.implementation.path; @@ -223,8 +226,7 @@ export class GatewayTargetPrimitive extends BasePrimitive> { + async add(options: AddMemoryOptions): Promise> { try { const strategies = options.strategies ? options.strategies @@ -83,17 +83,17 @@ export class MemoryPrimitive extends BasePrimitive { + async remove(memoryName: string): Promise { try { const project = await this.readProjectSpec(); const memoryIndex = project.memories.findIndex(m => m.name === memoryName); if (memoryIndex === -1) { - return { success: false, error: `Memory "${memoryName}" not found.` }; + return { success: false, error: new Error(`Memory "${memoryName}" not found.`) }; } project.memories.splice(memoryIndex, 1); @@ -101,8 +101,7 @@ export class MemoryPrimitive extends BasePrimitive> { + async add(options: AddOnlineEvalConfigOptions): Promise> { try { const config = await this.createOnlineEvalConfig(options); return { success: true, configName: config.name }; } catch (err) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) }; } } - async remove(configName: string): Promise { + async remove(configName: string): Promise { try { const project = await this.readProjectSpec(); const index = project.onlineEvalConfigs.findIndex(c => c.name === configName); if (index === -1) { - return { success: false, error: `Online eval config "${configName}" not found.` }; + return { success: false, error: new Error(`Online eval config "${configName}" not found.`) }; } project.onlineEvalConfigs.splice(index, 1); @@ -52,7 +52,7 @@ export class OnlineEvalConfigPrimitive extends BasePrimitive> { + async add(options: AddPolicyEngineOptions): Promise> { try { const project = await this.readProjectSpec(); @@ -40,17 +40,17 @@ export class PolicyEnginePrimitive extends BasePrimitive { + async remove(engineName: string): Promise { try { const project = await this.readProjectSpec(); const index = project.policyEngines.findIndex(e => e.name === engineName); if (index === -1) { - return { success: false, error: `Policy engine "${engineName}" not found.` }; + return { success: false, error: new Error(`Policy engine "${engineName}" not found.`) }; } project.policyEngines.splice(index, 1); @@ -70,8 +70,7 @@ export class PolicyEnginePrimitive extends BasePrimitive> { + async add(options: AddPolicyOptions): Promise> { try { const sourceFlags = [options.statement, options.source, options.generate].filter(Boolean); if (sourceFlags.length > 1) { return { success: false, - error: 'Only one of --statement, --source, or --generate can be provided.', + error: new Error('Only one of --statement, --source, or --generate can be provided.'), }; } @@ -48,7 +48,7 @@ export class PolicyPrimitive extends BasePrimitive e.name === options.engine); if (!engine) { - return { success: false, error: `Policy engine "${options.engine}" not found.` }; + return { success: false, error: new Error(`Policy engine "${options.engine}" not found.`) }; } this.checkDuplicate(engine.policies, options.name, 'Policy'); @@ -57,11 +57,11 @@ export class PolicyPrimitive extends BasePrimitive to specify one.', + error: new Error( + 'No deployed gateway found. Policy generation requires a deployed gateway. Use --gateway to specify one.' + ), }; } @@ -123,7 +124,7 @@ export class PolicyPrimitive extends BasePrimitive { + async remove(nameOrCompositeKey: string, engineName?: string): Promise { try { const project = await this.readProjectSpec(); @@ -167,7 +168,9 @@ export class PolicyPrimitive extends BasePrimitive 1) { return { success: false, - error: `Policy "${resolvedPolicy}" exists in multiple engines: ${matchingEngines.map(e => e.name).join(', ')}. Use --engine to specify which one.`, + error: new Error( + `Policy "${resolvedPolicy}" exists in multiple engines: ${matchingEngines.map(e => e.name).join(', ')}. Use --engine to specify which one.` + ), }; } } @@ -185,10 +188,12 @@ export class PolicyPrimitive extends BasePrimitive { + async add(options: AddRuntimeEndpointOptions): Promise>> { try { const project = await this.readProjectSpec(); // Find the parent runtime const runtime = project.runtimes.find(a => a.name === options.runtime); if (!runtime) { - return { success: false, error: `Runtime "${options.runtime}" not found.` }; + return { success: false, error: new Error(`Runtime "${options.runtime}" not found.`) }; } // Initialize endpoints dictionary if needed @@ -56,14 +56,14 @@ export class RuntimeEndpointPrimitive extends BasePrimitive deployedRuntime.runtimeVersion) { return { success: false, - error: `Version ${version} exceeds latest deployed version ${deployedRuntime.runtimeVersion} for runtime "${options.runtime}".`, + error: new Error( + `Version ${version} exceeds latest deployed version ${deployedRuntime.runtimeVersion} for runtime "${options.runtime}".` + ), }; } } @@ -104,11 +106,11 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { + async remove(name: string): Promise { try { const project = await this.readProjectSpec(); @@ -119,7 +121,7 @@ export class RuntimeEndpointPrimitive extends BasePrimitive r.name === runtimeName); if (!runtime?.endpoints?.[endpointName]) { - return { success: false, error: `Runtime endpoint "${name}" not found.` }; + return { success: false, error: new Error(`Runtime endpoint "${name}" not found.`) }; } delete runtime.endpoints[endpointName]; if (Object.keys(runtime.endpoints).length === 0) { @@ -141,9 +143,9 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { const result = await primitive.add(validOptions); - expect(result.success).toBe(true); - expect(result).toHaveProperty('abTestName', 'MyTest'); + expect(result).toEqual(expect.objectContaining({ success: true, abTestName: 'MyTest' })); const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; expect(writtenSpec.abTests).toHaveLength(1); @@ -121,7 +120,10 @@ describe('ABTestPrimitive', () => { const result = await primitive.add(validOptions); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('already exists') }), + }) ); }); @@ -130,7 +132,9 @@ describe('ABTestPrimitive', () => { const result = await primitive.add(validOptions); - expect(result).toEqual(expect.objectContaining({ success: false, error: 'disk read error' })); + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.objectContaining({ message: 'disk read error' }) }) + ); }); it('returns error when writeProjectSpec fails', async () => { @@ -139,7 +143,9 @@ describe('ABTestPrimitive', () => { const result = await primitive.add(validOptions); - expect(result).toEqual(expect.objectContaining({ success: false, error: 'disk write error' })); + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.objectContaining({ message: 'disk write error' }) }) + ); }); it('returns error when variant weights do not sum to 100', async () => { @@ -175,8 +181,8 @@ describe('ABTestPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('NonExistent'); - expect(result.error).toContain('not found'); + expect(result.error.message).toContain('NonExistent'); + expect(result.error.message).toContain('not found'); } }); @@ -187,7 +193,7 @@ describe('ABTestPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toBe('io error'); + expect(result.error.message).toBe('io error'); } }); diff --git a/src/cli/primitives/__tests__/BasePrimitive.test.ts b/src/cli/primitives/__tests__/BasePrimitive.test.ts index 830cd4244..55255191f 100644 --- a/src/cli/primitives/__tests__/BasePrimitive.test.ts +++ b/src/cli/primitives/__tests__/BasePrimitive.test.ts @@ -1,5 +1,5 @@ import { BasePrimitive } from '../BasePrimitive'; -import type { AddResult, AddScreenComponent, RemovableResource, RemovalPreview, RemovalResult } from '../types'; +import type { AddScreenComponent, RemovableResource, RemovalPreview, Result } from '../types'; import type { Command } from '@commander-js/extra-typings'; import { describe, expect, it } from 'vitest'; import { z } from 'zod'; @@ -10,11 +10,11 @@ class StubPrimitive extends BasePrimitive { readonly label = 'Stub'; readonly primitiveSchema = z.object({ name: z.string() }); - add(_options: Record): Promise { + add(_options: Record): Promise { return Promise.resolve({ success: true }); } - remove(_name: string): Promise { + remove(_name: string): Promise { return Promise.resolve({ success: true }); } diff --git a/src/cli/primitives/__tests__/EvaluatorPrimitive.test.ts b/src/cli/primitives/__tests__/EvaluatorPrimitive.test.ts index b41545d20..88e25e3c0 100644 --- a/src/cli/primitives/__tests__/EvaluatorPrimitive.test.ts +++ b/src/cli/primitives/__tests__/EvaluatorPrimitive.test.ts @@ -74,8 +74,7 @@ describe('EvaluatorPrimitive', () => { config: validConfig, }); - expect(result.success).toBe(true); - expect(result).toHaveProperty('evaluatorName', 'MyEval'); + expect(result).toEqual(expect.objectContaining({ success: true, evaluatorName: 'MyEval' })); const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; expect(writtenSpec.evaluators).toHaveLength(1); @@ -108,7 +107,10 @@ describe('EvaluatorPrimitive', () => { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('already exists') }), + }) ); }); @@ -121,7 +123,9 @@ describe('EvaluatorPrimitive', () => { config: validConfig, }); - expect(result).toEqual(expect.objectContaining({ success: false, error: 'disk read error' })); + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.objectContaining({ message: 'disk read error' }) }) + ); }); }); @@ -145,8 +149,8 @@ describe('EvaluatorPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('NonExistent'); - expect(result.error).toContain('not found'); + expect(result.error.message).toContain('NonExistent'); + expect(result.error.message).toContain('not found'); } }); @@ -159,8 +163,8 @@ describe('EvaluatorPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('referenced by online eval config'); - expect(result.error).toContain('MyOnlineConfig'); + expect(result.error.message).toContain('referenced by online eval config'); + expect(result.error.message).toContain('MyOnlineConfig'); } expect(mockWriteProjectSpec).not.toHaveBeenCalled(); }); @@ -172,7 +176,7 @@ describe('EvaluatorPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toBe('io error'); + expect(result.error.message).toBe('io error'); } }); }); diff --git a/src/cli/primitives/__tests__/OnlineEvalConfigPrimitive.test.ts b/src/cli/primitives/__tests__/OnlineEvalConfigPrimitive.test.ts index c81160a6c..814305711 100644 --- a/src/cli/primitives/__tests__/OnlineEvalConfigPrimitive.test.ts +++ b/src/cli/primitives/__tests__/OnlineEvalConfigPrimitive.test.ts @@ -52,8 +52,7 @@ describe('OnlineEvalConfigPrimitive', () => { samplingRate: 10, }); - expect(result.success).toBe(true); - expect(result).toHaveProperty('configName', 'MyConfig'); + expect(result).toEqual(expect.objectContaining({ success: true, configName: 'MyConfig' })); const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0]; expect(writtenSpec.onlineEvalConfigs).toHaveLength(1); @@ -126,7 +125,10 @@ describe('OnlineEvalConfigPrimitive', () => { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('already exists') }), + }) ); }); @@ -140,7 +142,9 @@ describe('OnlineEvalConfigPrimitive', () => { samplingRate: 10, }); - expect(result).toEqual(expect.objectContaining({ success: false, error: 'no project' })); + expect(result).toEqual( + expect.objectContaining({ success: false, error: expect.objectContaining({ message: 'no project' }) }) + ); }); }); @@ -169,8 +173,8 @@ describe('OnlineEvalConfigPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain('NonExistent'); - expect(result.error).toContain('not found'); + expect(result.error.message).toContain('NonExistent'); + expect(result.error.message).toContain('not found'); } }); @@ -181,7 +185,7 @@ describe('OnlineEvalConfigPrimitive', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toBe('io error'); + expect(result.error.message).toBe('io error'); } }); }); diff --git a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts index 46fe426f5..433d0c0e2 100644 --- a/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts +++ b/src/cli/primitives/__tests__/RuntimeEndpointPrimitive.test.ts @@ -87,7 +87,12 @@ describe('RuntimeEndpointPrimitive', () => { endpoint: 'prod', }); - expect(result).toEqual(expect.objectContaining({ success: false, error: expect.stringContaining('not found') })); + expect(result).toEqual( + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('not found') }), + }) + ); }); it('returns error when endpoint already exists', async () => { @@ -100,7 +105,10 @@ describe('RuntimeEndpointPrimitive', () => { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('already exists') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('already exists') }), + }) ); }); @@ -134,7 +142,10 @@ describe('RuntimeEndpointPrimitive', () => { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('positive integer') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('positive integer') }), + }) ); }); @@ -181,7 +192,10 @@ describe('RuntimeEndpointPrimitive', () => { }); expect(result).toEqual( - expect.objectContaining({ success: false, error: expect.stringContaining('exceeds latest deployed version') }) + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('exceeds latest deployed version') }), + }) ); }); }); @@ -223,7 +237,12 @@ describe('RuntimeEndpointPrimitive', () => { const result = await primitive.remove('MyRuntime/nonexistent'); - expect(result).toEqual(expect.objectContaining({ success: false, error: expect.stringContaining('not found') })); + expect(result).toEqual( + expect.objectContaining({ + success: false, + error: expect.objectContaining({ message: expect.stringContaining('not found') }), + }) + ); }); it('cleans up empty endpoints dict after removing last endpoint', async () => { diff --git a/src/cli/primitives/index.ts b/src/cli/primitives/index.ts index 3f69da1ed..b609bedd0 100644 --- a/src/cli/primitives/index.ts +++ b/src/cli/primitives/index.ts @@ -24,4 +24,4 @@ export { getPrimitive, } from './registry'; export { SOURCE_CODE_NOTE } from './constants'; -export type { AddResult, AddScreenComponent, RemovableResource, RemovalPreview, RemovalResult } from './types'; +export type { AddScreenComponent, RemovableResource, RemovalPreview, Result } from './types'; diff --git a/src/cli/primitives/types.ts b/src/cli/primitives/types.ts index 73842c972..09564a87a 100644 --- a/src/cli/primitives/types.ts +++ b/src/cli/primitives/types.ts @@ -1,14 +1,7 @@ -import type { RemovalPreview, RemovalResult } from '../operations/remove/types'; +import type { RemovalPreview } from '../operations/remove/types'; import type { ComponentType } from 'react'; -/** - * Result of an add operation. - * Use the generic parameter to type extra fields on the success branch: - * AddResult<{ agentName: string }> → success branch has typed agentName - */ -export type AddResult = Record> = - | ({ success: true; message?: string } & T) - | { success: false; error: string }; +export type { Result } from '../../lib/types'; /** * Represents a resource that can be removed. @@ -21,7 +14,7 @@ export interface RemovableResource { /** * Re-export removal types from shared types. */ -export type { RemovalPreview, RemovalResult }; +export type { RemovalPreview }; /** * Screen component type for TUI add flows. diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts index 987f05730..ce36a1f31 100644 --- a/src/cli/telemetry/cli-command-run.ts +++ b/src/cli/telemetry/cli-command-run.ts @@ -1,5 +1,5 @@ import { getErrorMessage } from '../errors'; -import type { AddResult } from '../primitives/types.js'; +import type { Result } from '../primitives/types.js'; import { TelemetryClientAccessor } from './client-accessor.js'; import type { Command, CommandAttrs } from './schemas/command-run.js'; @@ -44,8 +44,8 @@ export async function cliCommandRun( export async function withAddTelemetry>( command: C, attrs: CommandAttrs, - fn: () => Promise> -): Promise> { + fn: () => Promise> +): Promise> { let client; try { client = await TelemetryClientAccessor.get(); @@ -53,18 +53,18 @@ export async function withAddTelemetry | undefined; + let result: Result | undefined; try { await client.withCommandRun(command, async () => { result = await fn(); - if (!result.success) throw new Error(result.error); + if (!result.success) throw result.error; return attrs; }); } catch (err) { // withCommandRun re-throws after recording failure telemetry. // result is set if fn() ran; if not, fn() itself threw. if (!result) { - return { success: false, error: getErrorMessage(err) }; + return { success: false, error: err instanceof Error ? err : new Error(getErrorMessage(err)) } as Result; } } return result!; diff --git a/src/cli/tui/hooks/useCreateABTest.ts b/src/cli/tui/hooks/useCreateABTest.ts index e54666074..6a197ce59 100644 --- a/src/cli/tui/hooks/useCreateABTest.ts +++ b/src/cli/tui/hooks/useCreateABTest.ts @@ -43,7 +43,7 @@ export function useCreateABTest() { enableOnCreate: config.enableOnCreate, }); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create AB test'); + throw addResult.error ?? new Error('Failed to create AB test'); } setStatus({ state: 'success' }); return { ok: true as const, testName: config.name }; @@ -59,7 +59,7 @@ export function useCreateABTest() { try { const addResult = await abTestPrimitive.addTargetBased(config); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create target-based AB test'); + throw addResult.error ?? new Error('Failed to create target-based AB test'); } setStatus({ state: 'success' }); return { ok: true as const, testName: config.name }; diff --git a/src/cli/tui/hooks/useCreateConfigBundle.ts b/src/cli/tui/hooks/useCreateConfigBundle.ts index 864501eed..c1f9ade7e 100644 --- a/src/cli/tui/hooks/useCreateConfigBundle.ts +++ b/src/cli/tui/hooks/useCreateConfigBundle.ts @@ -25,7 +25,7 @@ export function useCreateConfigBundle() { commitMessage: config.commitMessage, }); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create configuration bundle'); + throw addResult.error ?? new Error('Failed to create configuration bundle'); } setStatus({ state: 'success' }); return { ok: true as const, bundleName: config.name }; diff --git a/src/cli/tui/hooks/useCreateEvaluator.ts b/src/cli/tui/hooks/useCreateEvaluator.ts index f1cad666f..29c67764b 100644 --- a/src/cli/tui/hooks/useCreateEvaluator.ts +++ b/src/cli/tui/hooks/useCreateEvaluator.ts @@ -32,7 +32,7 @@ export function useCreateEvaluator() { }) ); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create evaluator'); + throw addResult.error ?? new Error('Failed to create evaluator'); } setStatus({ state: 'success' }); return { ok: true as const, evaluatorName: config.name, codePath: addResult.codePath }; diff --git a/src/cli/tui/hooks/useCreateMcp.ts b/src/cli/tui/hooks/useCreateMcp.ts index ec91666d0..48fd12005 100644 --- a/src/cli/tui/hooks/useCreateMcp.ts +++ b/src/cli/tui/hooks/useCreateMcp.ts @@ -53,7 +53,7 @@ export function useCreateGateway() { }) ); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create gateway'); + throw addResult.error ?? new Error('Failed to create gateway'); } const result: CreateGatewayResult = { name: config.name }; setStatus({ state: 'success', result }); diff --git a/src/cli/tui/hooks/useCreateMemory.ts b/src/cli/tui/hooks/useCreateMemory.ts index d4196582f..8ec1bf8ec 100644 --- a/src/cli/tui/hooks/useCreateMemory.ts +++ b/src/cli/tui/hooks/useCreateMemory.ts @@ -45,7 +45,7 @@ export function useCreateMemory() { }) ); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create memory'); + throw addResult.error ?? new Error('Failed to create memory'); } // Read back the memory object const configIO = new ConfigIO(); diff --git a/src/cli/tui/hooks/useCreateOnlineEval.ts b/src/cli/tui/hooks/useCreateOnlineEval.ts index b853fed05..608cfe0aa 100644 --- a/src/cli/tui/hooks/useCreateOnlineEval.ts +++ b/src/cli/tui/hooks/useCreateOnlineEval.ts @@ -38,7 +38,7 @@ export function useCreateOnlineEval() { }) ); if (!addResult.success) { - throw new Error(addResult.error ?? 'Failed to create online eval config'); + throw addResult.error ?? new Error('Failed to create online eval config'); } setStatus({ state: 'success' }); return { ok: true as const, configName: config.name }; diff --git a/src/cli/tui/hooks/useRemove.ts b/src/cli/tui/hooks/useRemove.ts index 7682479d3..9809d8670 100644 --- a/src/cli/tui/hooks/useRemove.ts +++ b/src/cli/tui/hooks/useRemove.ts @@ -1,6 +1,6 @@ import type { ResourceType } from '../../commands/remove/types'; import { RemoveLogger } from '../../logging'; -import type { RemovableGatewayTarget, RemovalPreview, RemovalResult } from '../../operations/remove'; +import type { RemovableGatewayTarget, RemovalPreview, Result } from '../../operations/remove'; import type { RemovableCredential } from '../../primitives/CredentialPrimitive'; import type { RemovableMemory } from '../../primitives/MemoryPrimitive'; import type { RemovablePolicyResource } from '../../primitives/PolicyPrimitive'; @@ -60,7 +60,7 @@ function useRemovableResources(loader: () => Promise) { * All useRemove* hooks delegate to this. */ function useRemoveResource( - removeFn: (id: TIdentifier) => Promise, + removeFn: (id: TIdentifier) => Promise, resourceType: ResourceType, getResourceName: (id: TIdentifier) => string ) { @@ -83,7 +83,7 @@ function useRemoveResource( resourceType: resourceTypeRef.current, resourceName: getNameRef.current(id), }); - logger.logRemoval(preview, result.success, result.success ? undefined : result.error); + logger.logRemoval(preview, result.success, result.success ? undefined : result.error.message); logPath = logger.getAbsoluteLogPath(); setLogFilePath(logPath); } @@ -291,10 +291,10 @@ export function useRemovalPreview() { interface RemovalState { isLoading: boolean; - result: RemovalResult | null; + result: Result | null; } -type RemoveResult = RemovalResult & { logFilePath?: string }; +type RemoveResult = Result & { logFilePath?: string }; export function useRemoveAgent() { return useRemoveResource( diff --git a/src/cli/tui/screens/agent/useAddAgent.ts b/src/cli/tui/screens/agent/useAddAgent.ts index e2fabe140..e849528d4 100644 --- a/src/cli/tui/screens/agent/useAddAgent.ts +++ b/src/cli/tui/screens/agent/useAddAgent.ts @@ -12,6 +12,7 @@ import { executeImportAgent } from '../../../operations/agent/import'; import { buildAuthorizerConfigFromJwtConfig, createManagedOAuthCredential } from '../../../primitives/auth-utils'; import { computeDefaultCredentialEnvVarName } from '../../../primitives/credential-utils'; import { credentialPrimitive } from '../../../primitives/registry'; +import type { Result } from '../../../primitives/types'; import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; import { AgentType as AgentTypeEnum, @@ -165,7 +166,7 @@ export function useAddAgent() { () => addAgentInner(config) ); if (!result.success) { - return { ok: false, error: result.error }; + return { ok: false, error: result.error.message }; } return result.outcome; } finally { @@ -180,26 +181,24 @@ export function useAddAgent() { return { addAgent, isLoading, reset }; } -type AddAgentInnerResult = - | { success: true; outcome: AddAgentCreateResult | AddAgentByoResult } - | { success: false; error: string }; +type AddAgentInnerResult = Result<{ outcome: AddAgentCreateResult | AddAgentByoResult }>; async function addAgentInner(config: AddAgentConfig): Promise { const configBaseDir = findConfigRoot(); if (!configBaseDir) { - return { success: false, error: new NoProjectError().message }; + return { success: false, error: new NoProjectError() }; } const configIO = new ConfigIO({ baseDir: configBaseDir }); if (!configIO.configExists('project')) { - return { success: false, error: new NoProjectError().message }; + return { success: false, error: new NoProjectError() }; } const project = await configIO.readProjectSpec(); const existingAgent = project.runtimes.find(agent => agent.name === config.name); if (existingAgent) { - return { success: false, error: `Agent "${config.name}" already exists in this project.` }; + return { success: false, error: new Error(`Agent "${config.name}" already exists in this project.`) }; } let outcome: AddAgentCreateResult | AddAgentByoResult | AddAgentError; @@ -212,7 +211,7 @@ async function addAgentInner(config: AddAgentConfig): Promise [ ...prev, diff --git a/src/cli/tui/screens/eval/EvalScreen.tsx b/src/cli/tui/screens/eval/EvalScreen.tsx index 3f57999d9..b7cfc7efa 100644 --- a/src/cli/tui/screens/eval/EvalScreen.tsx +++ b/src/cli/tui/screens/eval/EvalScreen.tsx @@ -339,7 +339,7 @@ export function EvalScreen({ onExit }: EvalScreenProps) { } const result = handleListEvalRuns({}); if (!result.success) { - setState({ phase: 'error', runs: [], error: result.error ?? 'Unknown error' }); + setState({ phase: 'error', runs: [], error: result.error.message ?? 'Unknown error' }); return; } setState({ phase: 'loaded', runs: result.runs ?? [], error: null }); diff --git a/src/cli/tui/screens/identity/useCreateIdentity.ts b/src/cli/tui/screens/identity/useCreateIdentity.ts index e214d1e67..6f8095055 100644 --- a/src/cli/tui/screens/identity/useCreateIdentity.ts +++ b/src/cli/tui/screens/identity/useCreateIdentity.ts @@ -25,7 +25,7 @@ export function useCreateIdentity() { () => credentialPrimitive.add(config) ); if (!result.success) { - throw new Error(result.error ?? 'Failed to create credential'); + throw result.error ?? new Error('Failed to create credential'); } // Read back the credential object const configIO = new ConfigIO(); diff --git a/src/cli/tui/screens/import/ImportFlow.tsx b/src/cli/tui/screens/import/ImportFlow.tsx index ed82962d3..71a07c702 100644 --- a/src/cli/tui/screens/import/ImportFlow.tsx +++ b/src/cli/tui/screens/import/ImportFlow.tsx @@ -151,10 +151,10 @@ export function ImportFlow({ onBack, onNavigate }: ImportFlowProps) { Name: {result.resourceName} - {result.resourceId && ( + {(result.success ? result.resourceId : undefined) && ( ID: - {result.resourceId} + {result.success ? result.resourceId : undefined} )} diff --git a/src/cli/tui/screens/import/ImportProgressScreen.tsx b/src/cli/tui/screens/import/ImportProgressScreen.tsx index 3771ffbab..aa8c6f9e6 100644 --- a/src/cli/tui/screens/import/ImportProgressScreen.tsx +++ b/src/cli/tui/screens/import/ImportProgressScreen.tsx @@ -59,9 +59,9 @@ export function ImportProgressScreen({ onSuccess(result); } else { setSteps(prev => - prev.map(s => (s.status === 'running' ? { ...s, status: 'error', error: result.error } : s)) + prev.map(s => (s.status === 'running' ? { ...s, status: 'error', error: result.error.message } : s)) ); - onError(result.error ?? 'Import failed'); + onError(result.error.message); } } else { // Starter toolkit @@ -75,9 +75,9 @@ export function ImportProgressScreen({ onSuccess(result); } else { setSteps(prev => - prev.map(s => (s.status === 'running' ? { ...s, status: 'error', error: result.error } : s)) + prev.map(s => (s.status === 'running' ? { ...s, status: 'error', error: result.error.message } : s)) ); - onError(result.error ?? 'Import failed'); + onError(result.error.message); } } }; diff --git a/src/cli/tui/screens/online-eval/OnlineEvalDashboard.tsx b/src/cli/tui/screens/online-eval/OnlineEvalDashboard.tsx index 03961473e..06115a0fc 100644 --- a/src/cli/tui/screens/online-eval/OnlineEvalDashboard.tsx +++ b/src/cli/tui/screens/online-eval/OnlineEvalDashboard.tsx @@ -156,7 +156,7 @@ export function OnlineEvalDashboard({ onExit }: OnlineEvalDashboardProps) { setState(prev => ({ ...prev, phase: 'toggling' })); void handlePauseResume({ name: item.name }, action).then(result => { if (!result.success) { - setState(prev => ({ ...prev, phase: 'loaded', error: result.error ?? 'Toggle failed' })); + setState(prev => ({ ...prev, phase: 'loaded', error: result.error.message ?? 'Toggle failed' })); return; } return fetchDashboardConfigs().then(configs => { diff --git a/src/cli/tui/screens/policy/AddPolicyFlow.tsx b/src/cli/tui/screens/policy/AddPolicyFlow.tsx index 9b3542cb8..64eaa4f5c 100644 --- a/src/cli/tui/screens/policy/AddPolicyFlow.tsx +++ b/src/cli/tui/screens/policy/AddPolicyFlow.tsx @@ -139,7 +139,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD () => policyEnginePrimitive.add({ name: engineName }) ); if (!result.success) { - setFlow({ name: 'error', message: result.error }); + setFlow({ name: 'error', message: result.error.message }); return; } setEngineNames(prev => [...prev, engineName]); @@ -184,7 +184,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD setPolicyNames(prev => [...prev, config.name]); setFlow({ name: 'policy-success', policyName: config.name, engineName: config.engine }); } else { - setFlow({ name: 'error', message: result.error }); + setFlow({ name: 'error', message: result.error.message }); } }, []); diff --git a/src/cli/tui/screens/recommendation/RecommendationFlow.tsx b/src/cli/tui/screens/recommendation/RecommendationFlow.tsx index b28344df3..f3604b67e 100644 --- a/src/cli/tui/screens/recommendation/RecommendationFlow.tsx +++ b/src/cli/tui/screens/recommendation/RecommendationFlow.tsx @@ -33,7 +33,12 @@ type FlowState = recommendationId?: string; region?: string; } - | { name: 'results'; result: RunRecommendationCommandResult; config: RecommendationWizardConfig; filePath?: string } + | { + name: 'results'; + result: Extract; + config: RecommendationWizardConfig; + filePath?: string; + } | { name: 'creds-error'; message: string } | { name: 'error'; message: string; logFilePath?: string }; @@ -194,13 +199,17 @@ export function RecommendationFlow({ onExit }: RecommendationFlowProps) { setFlow(prev => { if (prev.name !== 'running') return prev; const steps = prev.steps.map(s => - s.status === 'running' ? { ...s, status: 'error' as const, error: result.error } : s + s.status === 'running' ? { ...s, status: 'error' as const, error: result.error.message } : s ); return { ...prev, steps }; }); await new Promise(resolve => setTimeout(resolve, 2000)); if (cancelled) return; - setFlow({ name: 'error', message: result.error ?? 'Recommendation failed', logFilePath: result.logFilePath }); + setFlow({ + name: 'error', + message: result.error.message ?? 'Recommendation failed', + logFilePath: result.logFilePath, + }); return; } @@ -333,7 +342,7 @@ export function RecommendationFlow({ onExit }: RecommendationFlowProps) { // ───────────────────────────────────────────────────────────────────────────── interface ResultsViewProps { - result: RunRecommendationCommandResult; + result: Extract; config: RecommendationWizardConfig; filePath?: string; onRunAnother: () => void; @@ -369,7 +378,7 @@ function ResultsView({ result, config, filePath, onRunAnother, onExit }: Results message: `New bundle version (${applyResult.newVersionId}) created with recommended changes. Local config updated.`, }); } else { - setApplyStatus({ applied: false, message: applyResult.error ?? 'Unknown error' }); + setApplyStatus({ applied: false, message: applyResult.error.message ?? 'Unknown error' }); } } catch (err) { setApplyStatus({ applied: false, message: getErrorMessage(err) }); diff --git a/src/cli/tui/screens/recommendation/RecommendationScreen.tsx b/src/cli/tui/screens/recommendation/RecommendationScreen.tsx index adac72ef8..eea776e50 100644 --- a/src/cli/tui/screens/recommendation/RecommendationScreen.tsx +++ b/src/cli/tui/screens/recommendation/RecommendationScreen.tsx @@ -152,7 +152,7 @@ export function RecommendationScreen({ const { region } = await detectRegion(); const agentResult = resolveAgent(context, { runtime: wizard.config.agent }); if (!agentResult.success) { - if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error }); + if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error.message }); return; } diff --git a/src/cli/tui/screens/remove/RemoveFlow.tsx b/src/cli/tui/screens/remove/RemoveFlow.tsx index 7001fde27..696107486 100644 --- a/src/cli/tui/screens/remove/RemoveFlow.tsx +++ b/src/cli/tui/screens/remove/RemoveFlow.tsx @@ -331,7 +331,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'agent-success', agentName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-agent', agentName, preview: result.preview }); @@ -353,7 +353,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'gateway-success', gatewayName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-gateway', gatewayName, preview: result.preview }); @@ -375,7 +375,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'tool-success', toolName: tool.name }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-gateway-target', tool, preview: result.preview }); @@ -397,7 +397,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'memory-success', memoryName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-memory', memoryName, preview: result.preview }); @@ -419,7 +419,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'identity-success', identityName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-identity', identityName, preview: result.preview }); @@ -441,7 +441,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'evaluator-success', evaluatorName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-evaluator', evaluatorName, preview: result.preview }); @@ -463,7 +463,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'online-eval-success', configName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-online-eval', configName, preview: result.preview }); @@ -485,7 +485,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'policy-engine-success', engineName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-policy-engine', engineName, preview: result.preview }); @@ -510,7 +510,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'policy-success', policyName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-policy', compositeKey, policyName, preview: result.preview }); @@ -532,7 +532,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'config-bundle-success', bundleName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-config-bundle', bundleName, preview: result.preview }); @@ -554,7 +554,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'ab-test-success', testName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-ab-test', testName, preview: result.preview }); @@ -576,7 +576,7 @@ export function RemoveFlow({ if (removeResult.success) { setFlow({ name: 'runtime-endpoint-success', endpointName }); } else { - setFlow({ name: 'error', message: removeResult.error }); + setFlow({ name: 'error', message: removeResult.error.message }); } } else { setFlow({ name: 'confirm-runtime-endpoint', endpointName, preview: result.preview }); @@ -662,7 +662,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'agent-success', agentName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -678,7 +678,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'gateway-success', gatewayName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -694,7 +694,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'tool-success', toolName: tool.name, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -710,7 +710,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'memory-success', memoryName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -726,7 +726,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'identity-success', identityName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -742,7 +742,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'evaluator-success', evaluatorName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -758,7 +758,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'online-eval-success', configName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -774,7 +774,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'policy-engine-success', engineName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -790,7 +790,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'policy-success', policyName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -806,7 +806,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'config-bundle-success', bundleName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -822,7 +822,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'ab-test-success', testName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, @@ -838,7 +838,7 @@ export function RemoveFlow({ if (result.success) { pendingResultRef.current = { name: 'runtime-endpoint-success', endpointName, logFilePath: result.logFilePath }; } else { - pendingResultRef.current = { name: 'error', message: result.error }; + pendingResultRef.current = { name: 'error', message: result.error.message }; } setResultReady(true); }, diff --git a/src/cli/tui/screens/run-eval/RunBatchEvalFlow.tsx b/src/cli/tui/screens/run-eval/RunBatchEvalFlow.tsx index 38c03c150..36bb09ec4 100644 --- a/src/cli/tui/screens/run-eval/RunBatchEvalFlow.tsx +++ b/src/cli/tui/screens/run-eval/RunBatchEvalFlow.tsx @@ -260,7 +260,7 @@ export function RunBatchEvalFlow({ onExit }: RunBatchEvalFlowProps) { setFlow(prev => { if (prev.name !== 'running') return prev; const steps = prev.steps.map(s => - s.status === 'running' ? { ...s, status: 'error' as const, error: result.error } : s + s.status === 'running' ? { ...s, status: 'error' as const, error: result.error.message } : s ); return { ...prev, steps }; }); @@ -268,7 +268,7 @@ export function RunBatchEvalFlow({ onExit }: RunBatchEvalFlowProps) { if (cancelled) return; setFlow({ name: 'error', - message: result.error ?? 'Batch evaluation failed', + message: result.error.message ?? 'Batch evaluation failed', logFilePath: result.logFilePath, }); return; @@ -472,7 +472,7 @@ function BatchEvalWizard({ agents, evaluators: rawEvaluators, onComplete, onExit const region = targetRegion ?? detectedRegion; const agentResult = resolveAgent(context, { runtime: config.agent }); if (!agentResult.success) { - if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error }); + if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error.message }); return; } @@ -846,7 +846,7 @@ function ResultsView({ result, savedFilePath, onRunAnother, onExit }: ResultsVie isActive: true, }); - const evalRes = result.evaluationResults; + const evalRes = result.success ? result.evaluationResults : undefined; const summaries = evalRes?.evaluatorSummaries; // Fall back to local grouping when API summaries aren't available diff --git a/src/cli/tui/screens/run-eval/RunEvalFlow.tsx b/src/cli/tui/screens/run-eval/RunEvalFlow.tsx index 6b4ced516..be9c94d08 100644 --- a/src/cli/tui/screens/run-eval/RunEvalFlow.tsx +++ b/src/cli/tui/screens/run-eval/RunEvalFlow.tsx @@ -20,7 +20,7 @@ type FlowState = | { name: 'loading' } | { name: 'wizard'; data: RunEvalFlowData } | { name: 'running'; config: RunEvalConfig } - | { name: 'results'; result: RunEvalResult; run: EvalRunResult } + | { name: 'results'; result: Extract; run: EvalRunResult } | { name: 'creds-error'; message: string } | { name: 'error'; message: string }; @@ -146,11 +146,11 @@ export function RunEvalFlow({ onExit, onViewRuns }: RunEvalFlowProps) { if (cancelled) return; if (!result.success || !result.run) { - setFlow({ name: 'error', message: result.error ?? 'Evaluation failed' }); + setFlow({ name: 'error', message: !result.success ? result.error.message : 'Evaluation failed' }); return; } - setFlow({ name: 'results', result, run: result.run }); + setFlow({ name: 'results', result: result, run: result.run }); } catch (err) { if (!cancelled) setFlow({ name: 'error', message: getErrorMessage(err) }); } diff --git a/src/cli/tui/screens/run-eval/RunEvalScreen.tsx b/src/cli/tui/screens/run-eval/RunEvalScreen.tsx index d98a7431c..aee0a4142 100644 --- a/src/cli/tui/screens/run-eval/RunEvalScreen.tsx +++ b/src/cli/tui/screens/run-eval/RunEvalScreen.tsx @@ -84,7 +84,7 @@ export function RunEvalScreen({ agents, evaluatorItems: rawEvaluatorItems, onCom const { region } = await detectRegion(); const agentResult = resolveAgent(context, { runtime: wizard.config.agent }); if (!agentResult.success) { - if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error }); + if (!cancelled) setSessionResult({ key: fetchKey, phase: 'error', message: agentResult.error.message }); return; } diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx index 83bda78e5..18c482e35 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx @@ -95,7 +95,7 @@ export function AddRuntimeEndpointFlow({ }); return; } - setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); + setFlow({ name: 'error', message: result.error?.message ?? 'Unknown error' }); }); }, []); diff --git a/src/cli/tui/screens/status/useStatusFlow.ts b/src/cli/tui/screens/status/useStatusFlow.ts index 2260e1e82..4861a9b0a 100644 --- a/src/cli/tui/screens/status/useStatusFlow.ts +++ b/src/cli/tui/screens/status/useStatusFlow.ts @@ -99,7 +99,7 @@ export function useStatusFlow() { ...prev, phase: 'ready', statusesLoaded: true, - statusesError: result.error, + statusesError: result.error.message, })); return; } diff --git a/src/lib/index.ts b/src/lib/index.ts index cab20d720..4cc958d04 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -25,5 +25,8 @@ export * from './errors'; export * from './packaging'; export * from './utils'; +// Types +export type { Result } from './types'; + // Schema I/O utilities export * from './schemas/io'; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 000000000..2c776e1fd --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,15 @@ +/** + * Discriminated union for fallible operations, inspired by Rust's Result. + * + * Success branch spreads T onto the result; failure branch carries an Error. + * E extends Error so callers always get stack traces, cause chains, and instanceof narrowing. + * + * @example + * Result // { success: true } | { success: false; error: Error } + * Result<{ name: string }> // { success: true; name: string } | { success: false; error: Error } + * Result<{ name: string }, ValidationError> // { success: true; name: string } | { success: false; error: ValidationError } + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type Result = {}, E extends Error = Error> = + | ({ success: true } & T) + | { success: false; error: E };