Skip to content
Open
105 changes: 6 additions & 99 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,114 +26,21 @@ import { registerTraces } from './commands/traces';
import { registerUpdate } from './commands/update';
import { registerValidate } from './commands/validate';
import { PACKAGE_VERSION } from './constants';
import { printPostCommandNotices, printTelemetryNotice } from './notices';
import { ALL_PRIMITIVES } from './primitives';
import { TelemetryClientAccessor } from './telemetry';
import { App } from './tui/App';
import { renderTUI, setupAltScreenCleanup } from './tui';
import { LayoutProvider } from './tui/context';
import { COMMAND_DESCRIPTIONS } from './tui/copy';
import { clearExitAction, getExitAction } from './tui/exit-action';
import { clearExitMessage, getExitMessage } from './tui/exit-message';
import { requireTTY } from './tui/guards';
import { CommandListScreen } from './tui/screens/home';
import { getCommandsForUI } from './tui/utils';
import { type UpdateCheckResult, checkForUpdate, printUpdateNotification } from './update-notifier';
import { checkForUpdate } from './update-notifier';
import { Command } from '@commander-js/extra-typings';
import { render } from 'ink';
import React from 'react';

// ANSI escape sequences
const ENTER_ALT_SCREEN = '\x1B[?1049h\x1B[H';
const EXIT_ALT_SCREEN = '\x1B[?1049l';
const SHOW_CURSOR = '\x1B[?25h';

// Track if we're in alternate screen mode
let inAltScreen = false;

/**
* Global terminal cleanup - ensures cursor is always restored on exit.
* Registered once at startup, catches all exit scenarios.
*/
function setupGlobalCleanup() {
const cleanup = () => {
if (inAltScreen) {
process.stdout.write(EXIT_ALT_SCREEN);
}
process.stdout.write(SHOW_CURSOR);
};

process.on('exit', cleanup);
process.on('SIGINT', () => {
cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
cleanup();
process.exit(0);
});
}

function printTelemetryNotice(): void {
const yellow = '\x1b[33m';
const reset = '\x1b[0m';
process.stderr.write(
[
'',
`${yellow}The AgentCore CLI will soon begin collecting aggregated, anonymous usage`,
'analytics to help improve the tool.',
'To opt out: agentcore telemetry disable',
`To learn more: agentcore telemetry --help${reset}`,
'',
'',
].join('\n')
);
}

function printPostCommandNotices(isFirstRun: boolean, updateCheck: Promise<UpdateCheckResult | null>): Promise<void> {
if (isFirstRun) {
printTelemetryNotice();
}
return updateCheck.then(result => {
if (result?.updateAvailable) {
printUpdateNotification(result);
}
});
}

/**
* Render the TUI in alternate screen buffer mode.
*/
function renderTUI(updateCheck: Promise<UpdateCheckResult | null>, isFirstRun: boolean) {
inAltScreen = true;
process.stdout.write(ENTER_ALT_SCREEN);

const { waitUntilExit } = render(React.createElement(App));

void waitUntilExit().then(async () => {
inAltScreen = false;
process.stdout.write(EXIT_ALT_SCREEN);
process.stdout.write(SHOW_CURSOR);

// Check if the TUI requested a post-exit action (e.g., launch browser dev mode)
const action = getExitAction();
clearExitAction();

if (action?.type === 'dev') {
const { launchBrowserDev } = await import('./commands/dev/browser-mode');
await launchBrowserDev();
return;
}

// Print any exit message set by screens (e.g., after successful project creation)
const exitMessage = getExitMessage();
if (exitMessage) {
console.log(exitMessage);
clearExitMessage();
}

await printPostCommandNotices(isFirstRun, updateCheck);
});
}

function renderHelp(program: Command): void {
const commands = getCommandsForUI(program);
render(React.createElement(LayoutProvider, null, React.createElement(CommandListScreen, { commands })));
Expand Down Expand Up @@ -214,7 +121,7 @@ export function registerCommands(program: Command) {

export const main = async (argv: string[]) => {
// Register global cleanup handlers once at startup
setupGlobalCleanup();
setupAltScreenCleanup();

// Generate installationId on first run and show telemetry notice
const { created: isFirstRun } = await getOrCreateInstallationId();
Expand All @@ -230,15 +137,15 @@ export const main = async (argv: string[]) => {
// Show TUI for no arguments, commander handles --help via configureHelp()
if (args.length === 0) {
requireTTY();
renderTUI(updateCheck, isFirstRun);
await renderTUI({ updateCheck, isFirstRun });
return;
}

if (isFirstRun) {
printTelemetryNotice();
}

TelemetryClientAccessor.init(args[0] ?? 'unknown');
await TelemetryClientAccessor.init(args[0] ?? 'unknown');
try {
await program.parseAsync(argv);
} finally {
Expand Down
21 changes: 8 additions & 13 deletions src/cli/commands/add/command.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { renderTUI } from '../../tui';
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
import { requireProject, requireTTY } from '../../tui/guards';
import { AddFlow } from '../../tui/screens/add/AddFlow';
import type { Command } from '@commander-js/extra-typings';
import { render } from 'ink';
import React from 'react';

export function registerAdd(program: Command): Command {
const addCmd = program
Expand All @@ -13,7 +11,7 @@ export function registerAdd(program: Command): Command {
.showSuggestionAfterError();

// Catch-all argument for invalid subcommands - Commander matches subcommands first
addCmd.argument('[subcommand]').action((subcommand: string | undefined, _options, cmd) => {
addCmd.argument('[subcommand]').action(async (subcommand: string | undefined, _options, cmd) => {
if (subcommand) {
console.error(`error: '${subcommand}' is not a valid subcommand.`);
cmd.outputHelp();
Expand All @@ -23,15 +21,12 @@ export function registerAdd(program: Command): Command {
requireProject();
requireTTY();

const { clear, unmount } = render(
<AddFlow
isInteractive={false}
onExit={() => {
clear();
unmount();
}}
/>
);
await renderTUI({
initialRoute: { name: 'add' },
enterAltScreen: false,
actionOnBack: 'exit',
isInteractive: false,
});
});

// Subcommands (agent, memory, credential, gateway, gateway-target) are registered
Expand Down
23 changes: 9 additions & 14 deletions src/cli/commands/create/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
BuildType as TelemetryBuildType,
standardize,
} from '../../telemetry/schemas/common-shapes.js';
import { renderTUI } from '../../tui';
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
import { requireTTY } from '../../tui/guards';
import { CreateScreen } from '../../tui/screens/create';
import { parseCommaSeparatedList } from '../shared/vpc-utils';
import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action';
import type { CreateOptions } from './types';
Expand All @@ -32,18 +32,13 @@ import type { Command } from '@commander-js/extra-typings';
import { Text, render } from 'ink';

/** Render CreateScreen for interactive TUI mode */
function handleCreateTUI(): void {
const cwd = getWorkingDirectory();
const { unmount } = render(
<CreateScreen
cwd={cwd}
isInteractive={false}
onExit={() => {
unmount();
process.exit(0);
}}
/>
);
function handleCreateTUI(): Promise<void> {
return renderTUI({
initialRoute: { name: 'create' },
enterAltScreen: false,
actionOnBack: 'exit',
isInteractive: false,
});
}

/** Print completion summary after successful create */
Expand Down Expand Up @@ -293,7 +288,7 @@ export const registerCreate = (program: Command) => {
await handleCreateCLI(options as CreateOptions);
} else {
requireTTY();
handleCreateTUI();
await handleCreateTUI();
}
} catch (error) {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
Expand Down
26 changes: 10 additions & 16 deletions src/cli/commands/deploy/command.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ConfigIO, serializeResult } from '../../../lib';
import { getErrorMessage } from '../../errors';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { renderTUI } from '../../tui';
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
import { requireProject, requireTTY } from '../../tui/guards';
import { DeployScreen } from '../../tui/screens/deploy/DeployScreen';
import { handleDeploy } from './actions';
import type { DeployOptions, DeployResult } from './types';
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from './utils';
Expand All @@ -14,20 +14,14 @@ import React from 'react';

const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

function handleDeployTUI(options: { autoConfirm?: boolean; diffMode?: boolean } = {}): void {
function handleDeployTUI(options: { diffMode?: boolean } = {}): Promise<void> {
requireProject();

const { unmount } = render(
<DeployScreen
isInteractive={false}
autoConfirm={options.autoConfirm}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diffMode={options.diffMode}
onExit={() => {
unmount();
process.exit(0);
}}
/>
);
return renderTUI({
initialRoute: { name: 'deploy', diffMode: options.diffMode },
enterAltScreen: false,
actionOnBack: 'exit',
isInteractive: false,
});
}
Comment thread
Hweinstock marked this conversation as resolved.

async function handleDeployCLI(options: DeployOptions): Promise<void> {
Expand Down Expand Up @@ -208,10 +202,10 @@ export const registerDeploy = (program: Command) => {
} else if (cliOptions.diff) {
// Diff-only: use TUI with diff mode
requireTTY();
handleDeployTUI({ diffMode: true });
await handleDeployTUI({ diffMode: true });
} else {
requireTTY();
handleDeployTUI();
await handleDeployTUI();
}
} catch (error) {
if (cliOptions.json) {
Expand Down
42 changes: 13 additions & 29 deletions src/cli/commands/invoke/command.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { type Result, ValidationError, serializeResult } from '../../../lib';
import { ValidationError, serializeResult } from '../../../lib';
import { getErrorMessage } from '../../errors';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { AgentProtocol, AuthType, standardize } from '../../telemetry/schemas/common-shapes.js';
import { renderTUI } from '../../tui';
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
import { requireProject, requireTTY } from '../../tui/guards';
import { InvokeScreen } from '../../tui/screens/invoke';
import { parseHeaderFlags } from '../shared/header-utils';
import { type InvokeContext, handleInvoke, loadInvokeConfig } from './action';
import { resolvePrompt } from './resolve-prompt';
import type { InvokeOptions, InvokeResult } from './types';
import { validateInvokeOptions } from './validate';
import type { Command } from '@commander-js/extra-typings';
import { Text, render } from 'ink';
import React from 'react';

const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

Expand Down Expand Up @@ -241,33 +240,18 @@ export const registerInvoke = (program: Command) => {
headers = parseHeaderFlags(cliOptions.header);
}

const tuiResult = await withCommandRunTelemetry(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

telemetry is moved inside the invoke logic since the client is now initialized in renderTUI.

'invoke',
{
has_stream: true,
has_session_id: !!cliOptions.sessionId,
auth_type: standardize(AuthType, cliOptions.bearerToken ? 'bearer_token' : 'sigv4'),
agent_protocol: standardize(AgentProtocol, resolveProtocol({}, agentProtocol)),
await renderTUI({
initialRoute: {
name: 'invoke',
sessionId: cliOptions.sessionId,
userId: cliOptions.userId,
headers,
bearerToken: cliOptions.bearerToken,
},
async (): Promise<Result> => {
const { waitUntilExit, unmount } = render(
<InvokeScreen
isInteractive={true}
onExit={() => unmount()}
initialSessionId={cliOptions.sessionId}
initialUserId={cliOptions.userId}
initialHeaders={headers}
initialBearerToken={cliOptions.bearerToken}
/>
);
await waitUntilExit();
return { success: true };
}
);
if (!tuiResult.success) {
render(<Text color="red">Error: {getErrorMessage(tuiResult.error)}</Text>);
process.exit(1);
}
enterAltScreen: false,
actionOnBack: 'exit',
isInteractive: false,
});
Comment thread
Hweinstock marked this conversation as resolved.
}
} catch (error) {
if (cliOptions.json) {
Expand Down
Loading
Loading