Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/cli/commands/deploy/progress.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigIO } from '../../../lib';
import { detectAwsContext } from '../../aws/aws-context';
import { ANSI } from '../../constants';
import { getErrorMessage } from '../../errors';
import { canSkipDeploy } from '../../operations/deploy/change-detection';
import { handleDeploy } from './actions';
Expand All @@ -18,7 +19,7 @@ export function createSpinnerProgress(): SpinnerProgress {
if (spinner) {
clearInterval(spinner);
spinner = undefined;
process.stdout.write('\r\x1b[K');
process.stdout.write(`\r${ANSI.clearLine}`);
}
};

Expand Down Expand Up @@ -91,14 +92,16 @@ export async function runCliDeploy(): Promise<void> {
}
console.log('');
} else {
console.warn(`\x1b[33mDeploy failed: ${result.error}. Starting dev server anyway...\x1b[0m`);
console.warn(`${ANSI.yellow}Deploy failed: ${result.error}. Starting dev server anyway...${ANSI.reset}`);
if (result.logPath) {
console.warn(`Deploy log: ${result.logPath}`);
}
console.log('');
}
} catch (deployErr) {
cleanup();
console.warn(`\x1b[33mDeploy failed: ${getErrorMessage(deployErr)}. Starting dev server anyway...\x1b[0m\n`);
console.warn(
`${ANSI.yellow}Deploy failed: ${getErrorMessage(deployErr)}. Starting dev server anyway...${ANSI.reset}\n`
);
}
}
5 changes: 2 additions & 3 deletions src/cli/commands/dev/browser-mode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigIO, findConfigRoot, getWorkingDirectory } from '../../../lib';
import type { AgentCoreProjectSpec } from '../../../schema';
import { ANSI } from '../../constants';
import { isPreviewEnabled } from '../../feature-flags';
import { getDevConfig, getDevSupportedAgents, loadDevEnv, loadProjectConfig } from '../../operations/dev';
import { type OtelCollector, startOtelCollector } from '../../operations/dev/otel';
Expand Down Expand Up @@ -312,9 +313,7 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
});
}

const ENTER_ALT_SCREEN = '\x1B[?1049h\x1B[H';
const EXIT_ALT_SCREEN = '\x1B[?1049l';
const SHOW_CURSOR = '\x1B[?25h';
const { enterAltScreen: ENTER_ALT_SCREEN, exitAltScreen: EXIT_ALT_SCREEN, showCursor: SHOW_CURSOR } = ANSI;

interface TuiPickerResult {
agentName?: string;
Expand Down
5 changes: 3 additions & 2 deletions src/cli/commands/import/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '../../../schema';
import { validateAwsCredentials } from '../../aws/account';
import { arnPrefix } from '../../aws/partition';
import { ANSI } from '../../constants';
import { ExecLogger } from '../../logging';
import { setupPythonProject } from '../../operations/python/setup';
import { executeCdkImportPipeline } from './import-pipeline';
Expand Down Expand Up @@ -315,8 +316,8 @@ export async function handleImport(options: ImportOptions): Promise<ImportResult
const cdkEnvVar = `MEMORY_${mem.name.toUpperCase().replace(/[.-]/g, '_')}_ID`;
const warnMsg =
`Warning: Memory "${mem.name}" env var must be updated in your agent code:\n` +
` \x1b[31m- MEMORY_ID = os.getenv("BEDROCK_AGENTCORE_MEMORY_ID")\x1b[0m\n` +
` \x1b[32m+ MEMORY_ID = os.getenv("${cdkEnvVar}")\x1b[0m`;
` ${ANSI.red}- MEMORY_ID = os.getenv("BEDROCK_AGENTCORE_MEMORY_ID")${ANSI.reset}\n` +
` ${ANSI.green}+ MEMORY_ID = os.getenv("${cdkEnvVar}")${ANSI.reset}`;
logger.log(`Memory "${mem.name}" env var must be updated: use ${cdkEnvVar}`, 'warn');
onProgress?.(warnMsg);
}
Expand Down
8 changes: 4 additions & 4 deletions src/cli/commands/import/command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ValidationError } from '../../../lib';
import { ANSI } from '../../constants';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { handleImport } from './actions';
import { ANSI } from './constants';
import { registerImportEvaluator } from './import-evaluator';
import { registerImportGateway } from './import-gateway';
import { registerImportMemory } from './import-memory';
Expand All @@ -10,7 +10,7 @@ import { registerImportRuntime } from './import-runtime';
import type { Command } from '@commander-js/extra-typings';
import * as fs from 'node:fs';

const { green, yellow, cyan, dim, reset } = ANSI;
const { red, green, yellow, cyan, dim, reset } = ANSI;

export const registerImport = (program: Command) => {
const importCmd = program
Expand Down Expand Up @@ -86,7 +86,7 @@ export const registerImport = (program: Command) => {
yes: cliOptions.yes,
onProgress: (message: string) => {
// Collect warnings for end-of-output display
if (message.includes('Warning') || message.includes('\x1b[33m')) {
if (message.startsWith('Warning')) {
warnings.push(message);
return;
}
Expand Down Expand Up @@ -144,7 +144,7 @@ export const registerImport = (program: Command) => {
console.log(`Log: ${result.logPath}`);
}
} else {
console.error(`\n\x1b[31m[error]${reset} Import failed: ${result.error.message}`);
console.error(`\n${red}[error]${reset} Import failed: ${result.error.message}`);
if (result.logPath) {
console.error(`Log: ${result.logPath}`);
}
Expand Down
10 changes: 0 additions & 10 deletions src/cli/commands/import/constants.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
/** Name validation regex used by all import handlers. */
export const NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/;

/** ANSI escape codes for console output. */
export const ANSI = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
dim: '\x1b[2m',
reset: '\x1b[0m',
} as const;

/**
* CloudFormation resource type to identifier key mapping for IMPORT.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
listAllEvaluators,
listAllOnlineEvaluationConfigs,
} from '../../aws/agentcore-control';
import { ANSI } from '../../constants';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ANSI } from './constants';
import { failResult, parseAndValidateArn } from './import-utils';
import { executeResourceImport } from './resource-import';
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import {
listAllGatewayTargets,
listAllGateways,
} from '../../aws/agentcore-control';
import { ANSI } from '../../constants';
import { isAccessDeniedError } from '../../errors';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ANSI } from './constants';
import { executeCdkImportPipeline } from './import-pipeline';
import {
failResult,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { Memory } from '../../../schema';
import { IndexedKeyTypeSchema } from '../../../schema';
import type { MemoryDetail, MemorySummary } from '../../aws/agentcore-control';
import { getMemoryDetail, listAllMemories } from '../../aws/agentcore-control';
import { ANSI } from '../../constants';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ANSI } from './constants';
import { parseAndValidateArn } from './import-utils';
import { executeResourceImport } from './resource-import';
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-online-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
listAllOnlineEvaluationConfigs,
} from '../../aws/agentcore-control';
import { arnPrefix } from '../../aws/partition';
import { ANSI } from '../../constants';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ANSI } from './constants';
import { failResult, findResourceInDeployedState, parseAndValidateArn } from './import-utils';
import { executeResourceImport } from './resource-import';
import type { ImportResourceOptions, ImportResourceResult, ResourceImportDescriptor } from './types';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { AgentEnvSpec } from '../../../schema';
import type { AgentRuntimeDetail, AgentRuntimeSummary } from '../../aws/agentcore-control';
import { getAgentRuntimeDetail, listAllAgentRuntimes } from '../../aws/agentcore-control';
import { ANSI } from '../../constants';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ANSI } from './constants';
import { copyAgentSource, failResult, parseAndValidateArn } from './import-utils';
import { executeResourceImport } from './resource-import';
import type { ImportResourceResult, ResourceImportDescriptor, RuntimeImportOptions } from './types';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import/import-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { APP_DIR, ConfigIO, NoProjectError, ValidationError, findConfigRoot } from '../../../lib';
import type { AwsDeploymentTarget } from '../../../schema';
import { detectAccount, validateAwsCredentials } from '../../aws/account';
import { ANSI } from '../../constants';
import { ExecLogger } from '../../logging';
import { setupPythonProject } from '../../operations/python/setup';
import { getTemplatePath } from '../../templates/templateRoot';
import { ANSI } from './constants';
import type { ImportResourceOptions, ImportResourceResult, ImportableResourceType } from './types';
import * as fs from 'node:fs';
import * as path from 'node:path';
Expand Down
3 changes: 2 additions & 1 deletion src/cli/commands/invoke/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
mcpListTools,
} from '../../aws';
import { invokeHarness } from '../../aws/agentcore-harness';
import { ANSI } from '../../constants';
import { isPreviewEnabled } from '../../feature-flags';
import { InvokeLogger } from '../../logging';
import { formatMcpToolList } from '../../operations/dev/utils';
Expand Down Expand Up @@ -131,7 +132,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption
// Warn about VPC mode endpoint requirements
if (agentSpec.networkMode === 'VPC') {
console.log(
'\x1b[33mWarning: This agent uses VPC network mode. Ensure your VPC endpoints are configured for invocation.\x1b[0m'
`${ANSI.yellow}Warning: This agent uses VPC network mode. Ensure your VPC endpoints are configured for invocation.${ANSI.reset}`
);
}

Expand Down
30 changes: 30 additions & 0 deletions src/cli/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,33 @@ export const SCHEMA_VERSION = 1;
* Default runtime endpoint name used in log group paths and console URLs.
*/
export const DEFAULT_ENDPOINT_NAME = 'DEFAULT';

/**
* Color gating: emit ANSI codes only when both streams are attached to a terminal.
* Uses AND so that redirecting either stream (e.g. `2> log.txt`) disables colors,
* preventing escape codes from leaking into files.
* - FORCE_COLOR: override to enable colors regardless of TTY (e.g. in CI/tests).
* - NO_COLOR: standard (no-color.org) override to strip all color output.
*/
const stylingEnabled =
!!process.env.FORCE_COLOR || (!process.env.NO_COLOR && !!process.stdout.isTTY && !!process.stderr.isTTY);
const style = (code: string) => (stylingEnabled ? code : '');

/** ANSI escape codes for console output. Auto-disabled when piped or NO_COLOR is set. */
export const ANSI = {
red: style('\x1b[31m'),
green: style('\x1b[32m'),
yellow: style('\x1b[33m'),
cyan: style('\x1b[36m'),
dim: style('\x1b[2m'),
reset: style('\x1b[0m'),
// Terminal control sequences (always active — only used when TTY is confirmed)
clearLine: '\x1b[K',
enterAltScreen: '\x1b[?1049h\x1b[H',
exitAltScreen: '\x1b[?1049l',
showCursor: '\x1b[?25h',
// TUI gradient colors
brightYellow: style('\x1b[38;2;255;255;0m'),
mutedYellow: style('\x1b[38;2;255;255;85m'),
darkYellow: style('\x1b[38;2;218;218;0m'),
} as const;
4 changes: 2 additions & 2 deletions src/cli/notices.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ANSI } from './constants';
import { type UpdateCheckResult, printUpdateNotification } from './update-notifier';

export function printTelemetryNotice(): void {
const yellow = '\x1b[33m';
const reset = '\x1b[0m';
const { yellow, reset } = ANSI;
process.stderr.write(
[
'',
Expand Down
5 changes: 2 additions & 3 deletions src/cli/tui/render.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ANSI } from '../constants';
import { printPostCommandNotices } from '../notices';
import { TelemetryClientAccessor } from '../telemetry';
import { type UpdateCheckResult } from '../update-notifier';
Expand All @@ -7,9 +8,7 @@ import { clearExitMessage, getExitMessage } from './exit-message';
import { render } from 'ink';
import React from 'react';

const ENTER_ALT_SCREEN = '\x1B[?1049h\x1B[H';
const EXIT_ALT_SCREEN = '\x1B[?1049l';
const SHOW_CURSOR = '\x1B[?25h';
const { enterAltScreen: ENTER_ALT_SCREEN, exitAltScreen: EXIT_ALT_SCREEN, showCursor: SHOW_CURSOR } = ANSI;

let inAltScreen = false;

Expand Down
7 changes: 4 additions & 3 deletions src/cli/tui/screens/invoke/useInvokeFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
mcpListTools,
} from '../../../aws';
import { invokeHarness } from '../../../aws/agentcore-harness';
import { ANSI } from '../../../constants';
import { getErrorMessage } from '../../../errors';
import { isPreviewEnabled } from '../../../feature-flags';
import { InvokeLogger } from '../../../logging';
Expand Down Expand Up @@ -379,7 +380,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState
const serverName = event.start.toolUse.serverName;
const label = serverName ? `${serverName}/${pendingToolName}` : pendingToolName;
logger?.logInfo(`Tool call: ${pendingToolName} (id: ${pendingToolUseId})`);
streamingContentRef.current += `\n\x1b[2m${label}`;
streamingContentRef.current += `\n${ANSI.dim}${label}`;
const currentContent = streamingContentRef.current;
setMessages(prev => {
const updated = [...prev];
Expand All @@ -391,7 +392,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState
});
} else if (event.start.type === 'toolResult') {
const status = event.start.toolResult.status;
const icon = status === 'error' ? ' \x1b[31m[error]\x1b[0m' : ' [ok]\x1b[0m';
const icon = status === 'error' ? ` ${ANSI.red}[error]${ANSI.reset}` : ` [ok]${ANSI.reset}`;
logger?.logInfo(`Tool result (${pendingToolName}): status=${status ?? 'success'}`);
streamingContentRef.current += `${icon}\n`;
const currentContent = streamingContentRef.current;
Expand Down Expand Up @@ -438,7 +439,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState

if (lastMetadata) {
const latency = (lastMetadata.latencyMs / 1000).toFixed(1);
streamingContentRef.current += `\n\x1b[2m${lastMetadata.inputTokens} in / ${lastMetadata.outputTokens} out / ${latency}s\x1b[0m`;
streamingContentRef.current += `\n${ANSI.dim}${lastMetadata.inputTokens} in / ${lastMetadata.outputTokens} out / ${latency}s${ANSI.reset}`;
const currentContent = streamingContentRef.current;
setMessages(prev => {
const updated = [...prev];
Expand Down
38 changes: 22 additions & 16 deletions src/cli/tui/utils/__tests__/gradient.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import { createGradient } from '../gradient.js';
import { describe, expect, it } from 'vitest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

describe('createGradient', () => {
it('returns a string containing the original characters', () => {
const result = createGradient('Hello');
// Strip ANSI escape codes to verify original text
// eslint-disable-next-line no-control-regex
const stripped = result.replace(/\x1b\[[0-9;]*m/g, '');
expect(stripped).toBe('Hello');
let originalForceColor: string | undefined;

beforeAll(() => {
originalForceColor = process.env.FORCE_COLOR;
process.env.FORCE_COLOR = '1';
});

afterAll(() => {
if (originalForceColor === undefined) {
delete process.env.FORCE_COLOR;
} else {
process.env.FORCE_COLOR = originalForceColor;
}
});

it('wraps each character with ANSI color codes', () => {
it('wraps each character with ANSI color codes', async () => {
const { createGradient } = await import('../gradient.js');
const result = createGradient('AB');
// Should contain escape sequences
expect(result).toContain('\x1b[');
// Should contain the reset code
expect(result).toContain('\x1b[0m');
});

it('handles empty string', () => {
it('handles empty string', async () => {
const { createGradient } = await import('../gradient.js');
expect(createGradient('')).toBe('');
});

it('handles single character', () => {
it('handles single character', async () => {
const { createGradient } = await import('../gradient.js');
const result = createGradient('X');
// eslint-disable-next-line no-control-regex
const stripped = result.replace(/\x1b\[[0-9;]*m/g, '');
expect(stripped).toBe('X');
});

it('produces different colors for characters at different positions', () => {
// With a long enough string, we should see multiple different color codes
it('produces different colors for characters at different positions', async () => {
const { createGradient } = await import('../gradient.js');
const result = createGradient('ABCDEFGHIJKLMNOP');
// Count distinct escape sequences (excluding reset)
// eslint-disable-next-line no-control-regex
const codes = result.match(/\x1b\[(?!0m)[0-9;]*m/g) ?? [];
const unique = new Set(codes);
Expand Down
15 changes: 8 additions & 7 deletions src/cli/tui/utils/gradient.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { ANSI } from '../../constants';

export function createGradient(text: string): string {
const colors = [
'\x1b[33m', // Standard ANSI Yellow (matches Ink's yellow)
'\x1b[38;2;255;255;0m', // Bright Yellow
'\x1b[38;2;255;255;85m', // Slightly muted
'\x1b[38;2;218;218;0m', // Darker yellow
'\x1b[33m', // Back to ANSI Yellow
ANSI.yellow, // Standard ANSI Yellow (matches Ink's yellow)
ANSI.brightYellow,
ANSI.mutedYellow,
ANSI.darkYellow,
ANSI.yellow, // Back to ANSI Yellow
];

const reset = '\x1b[0m';
const chars = text.split('');

return chars
.map((char, i) => {
// Distributes the yellow hues across the length of the string
const colorIndex = Math.floor((i / chars.length) * (colors.length - 1));
return colors[colorIndex] + char + reset;
return colors[colorIndex] + char + ANSI.reset;
})
.join('');
}
Loading
Loading