import { findConfigRoot, getWorkingDirectory } from '../../../lib'; import { getErrorMessage } from '../../errors'; import { detectContainerRuntime } from '../../external-requirements'; import { ExecLogger } from '../../logging'; import { callMcpTool, createDevServer, findAvailablePort, getAgentPort, getDevConfig, getDevSupportedAgents, getEndpointUrl, invokeAgent, invokeAgentStreaming, invokeForProtocol, listMcpTools, loadDevEnv, loadProjectConfig, } from '../../operations/dev'; import { OtelCollector, startOtelCollector } from '../../operations/dev/otel'; import { FatalError } from '../../tui/components'; import { LayoutProvider } from '../../tui/context'; import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; import { requireProject } from '../../tui/guards'; import { parseHeaderFlags } from '../shared/header-utils'; import { runBrowserMode } from './browser-mode'; import type { Command } from '@commander-js/extra-typings'; import { spawn } from 'child_process'; import { Text, render } from 'ink'; import path from 'node:path'; import React from 'react'; // Alternate screen buffer - same as main TUI const ENTER_ALT_SCREEN = '\x1B[?1049h\x1B[H'; const EXIT_ALT_SCREEN = '\x1B[?1049l'; const SHOW_CURSOR = '\x1B[?25h'; async function invokeDevServer( port: number, prompt: string, stream: boolean, headers?: Record ): Promise { try { if (stream) { // Stream response to stdout for await (const chunk of invokeAgentStreaming({ port, message: prompt, headers })) { process.stdout.write(chunk); } process.stdout.write('\n'); } else { const response = await invokeAgent({ port, message: prompt, headers }); console.log(response); } } catch (err) { if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } process.exit(1); } } async function invokeA2ADevServer(port: number, prompt: string, headers?: Record): Promise { try { for await (const chunk of invokeForProtocol('A2A', { port, message: prompt, headers })) { process.stdout.write(chunk); } process.stdout.write('\n'); } catch (err) { if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } process.exit(1); } } function isConnectionRefused(err: unknown): boolean { if (!(err instanceof Error)) return false; // ConnectionError from invoke.ts wraps fetch failures after retries if (err.name === 'ConnectionError') return true; const msg = err.message + (err.cause instanceof Error ? err.cause.message : ''); return msg.includes('ECONNREFUSED') || msg.includes('fetch failed'); } async function handleMcpInvoke( port: number, invokeValue: string, toolName?: string, input?: string, headers?: Record ): Promise { try { if (invokeValue === 'list-tools') { const { tools } = await listMcpTools(port, undefined, headers); if (tools.length === 0) { console.log('No tools available.'); return; } console.log('Available tools:'); for (const tool of tools) { const desc = tool.description ? ` - ${tool.description}` : ''; console.log(` ${tool.name}${desc}`); } } else if (invokeValue === 'call-tool') { if (!toolName) { console.error('Error: --tool is required with call-tool'); console.error('Usage: agentcore dev call-tool --tool --input \'{"arg": "value"}\''); process.exit(1); } // Initialize session first, then call tool with the session ID const { sessionId } = await listMcpTools(port, undefined, headers); let args: Record = {}; if (input) { try { args = JSON.parse(input) as Record; } catch { console.error(`Error: Invalid JSON for --input: ${input}`); console.error('Expected format: --input \'{"key": "value"}\''); process.exit(1); } } const result = await callMcpTool(port, toolName, args, sessionId, undefined, headers); console.log(result); } else { console.error(`Error: Unknown MCP invoke command "${invokeValue}"`); console.error('Usage:'); console.error(' agentcore dev list-tools'); console.error(' agentcore dev call-tool --tool --input \'{"arg": "value"}\''); process.exit(1); } } catch (err) { if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } process.exit(1); } } async function execInContainer(command: string, containerName: string): Promise { const detection = await detectContainerRuntime(); if (!detection.runtime) { console.error('Error: No container runtime found (docker, podman, or finch required)'); process.exit(1); } return new Promise((resolve, reject) => { const child = spawn(detection.runtime!.binary, ['exec', containerName, 'bash', '-c', command], { stdio: 'inherit', }); child.on('error', reject); child.on('close', code => { if (code !== 0 && code !== null) { process.exit(code); } resolve(); }); }); } export const registerDev = (program: Command) => { program .command('dev') .alias('d') .description(COMMAND_DESCRIPTIONS.dev) .argument('[prompt]', 'Send a prompt to a running dev server [non-interactive]') .option('-p, --port ', 'Port for development server', '8080') .option('-r, --runtime ', 'Runtime to run or invoke (required if multiple runtimes)') .option('-s, --stream', 'Stream response when invoking [non-interactive]') .option('-l, --logs', 'Run dev server with logs to stdout [non-interactive]') .option('--exec', 'Execute a shell command in the running dev container (Container agents only) [non-interactive]') .option('--tool ', 'MCP tool name (used with "call-tool" prompt) [non-interactive]') .option('--input ', 'MCP tool arguments as JSON (used with --tool) [non-interactive]') .option( '-H, --header
', 'Custom header to forward to the agent (format: "Name: Value", repeatable) [non-interactive]', (val: string, prev: string[]) => [...prev, val], [] as string[] ) .option('-b, --no-browser', 'Use terminal TUI instead of web-based chat UI') .option('--no-traces', 'Disable local OTEL trace collection') .action(async (positionalPrompt: string | undefined, opts) => { try { const port = parseInt(opts.port, 10); // Parse custom headers let headers: Record | undefined; if (opts.header && opts.header.length > 0) { headers = parseHeaderFlags(opts.header); } // Exec mode: run shell command in the dev container if (opts.exec) { if (!positionalPrompt) { console.error('A command is required with --exec. Usage: agentcore dev --exec "whoami"'); process.exit(1); } const workingDir = getWorkingDirectory(); const project = await loadProjectConfig(workingDir); const agentName = opts.runtime ?? project?.runtimes[0]?.name ?? 'unknown'; const targetAgent = project?.runtimes.find(a => a.name === agentName); if (targetAgent?.build !== 'Container') { console.error('Error: --exec is only supported for Container build agents.'); console.error('For CodeZip agents, use your terminal to run commands directly.'); process.exit(1); } const containerName = `agentcore-dev-${agentName}`.toLowerCase(); await execInContainer(positionalPrompt, containerName); return; } // If a prompt is provided, invoke a running dev server const invokePrompt = positionalPrompt; if (invokePrompt !== undefined) { const workingDir = getWorkingDirectory(); const invokeProject = await loadProjectConfig(workingDir); // Determine which agent/port to invoke let invokePort = port; let targetAgent = invokeProject?.runtimes[0]; if (opts.runtime && invokeProject) { invokePort = getAgentPort(invokeProject, opts.runtime, port); targetAgent = invokeProject.runtimes.find(a => a.name === opts.runtime); } else if (invokeProject && invokeProject.runtimes.length > 1 && !opts.runtime) { const names = invokeProject.runtimes.map(a => a.name).join(', '); console.error(`Error: Multiple runtimes found. Use --runtime to specify which one.`); console.error(`Available: ${names}`); process.exit(1); } const protocol = targetAgent?.protocol ?? 'HTTP'; // Override port for protocols with fixed framework ports if (protocol === 'A2A') invokePort = 9000; else if (protocol === 'MCP') invokePort = 8000; // Protocol-aware dispatch if (protocol === 'MCP') { await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers); } else if (protocol === 'A2A') { await invokeA2ADevServer(invokePort, invokePrompt, headers); } else { await invokeDevServer(invokePort, invokePrompt, opts.stream ?? false, headers); } return; } requireProject(); const workingDir = getWorkingDirectory(); const project = await loadProjectConfig(workingDir); if (!project) { render(); process.exit(1); } if (!project.runtimes || project.runtimes.length === 0) { render(); process.exit(1); } // Warn about VPC mode limitations in local dev const targetDevAgent = opts.runtime ? project.runtimes.find(a => a.name === opts.runtime) : project.runtimes[0]; if (targetDevAgent?.networkMode === 'VPC') { console.log( '\x1b[33mWarning: This agent uses VPC network mode. Local dev server runs outside your VPC. Network behavior may differ from deployed environment.\x1b[0m\n' ); } const supportedAgents = getDevSupportedAgents(project); if (supportedAgents.length === 0) { render( ); process.exit(1); } // Start local OTEL collector so agent traces are captured in dev mode. // Persists traces to .cli/traces/ so they survive dev server restarts. const configRoot = findConfigRoot(workingDir); let otelEnvVars: Record = {}; let collector: OtelCollector | undefined; if (opts.traces !== false) { const persistTracesDir = path.join(configRoot ?? workingDir, '.cli', 'traces'); const otelResult = await startOtelCollector(persistTracesDir); collector = otelResult.collector; otelEnvVars = otelResult.otelEnvVars; } // If --logs provided, run non-interactive mode if (opts.logs) { // Require --agent if multiple agents if (project.runtimes.length > 1 && !opts.runtime) { const names = project.runtimes.map(a => a.name).join(', '); console.error(`Error: Multiple runtimes found. Use --runtime to specify which one.`); console.error(`Available: ${names}`); process.exit(1); } const agentName = opts.runtime ?? project.runtimes[0]?.name; const { envVars } = await loadDevEnv(workingDir); const mergedEnvVars = { ...envVars, ...otelEnvVars }; const config = getDevConfig(workingDir, project, configRoot ?? undefined, agentName); if (!config) { console.error('Error: No dev-supported agents found.'); process.exit(1); } // Create logger for log file path const logger = new ExecLogger({ command: 'dev' }); // Calculate port: A2A/MCP use fixed framework ports, HTTP uses configurable port const isA2A = config.protocol === 'A2A'; const isMcp = config.protocol === 'MCP'; const fixedPort = isA2A ? 9000 : isMcp ? 8000 : getAgentPort(project, config.agentName, port); const actualPort = await findAvailablePort(fixedPort); if ((isA2A || isMcp) && actualPort !== fixedPort) { console.error(`Error: Port ${fixedPort} is in use. ${config.protocol} agents require port ${fixedPort}.`); process.exit(1); } if (actualPort !== fixedPort) { console.log(`Port ${fixedPort} in use, using ${actualPort}`); } // Get provider info from agent config const providerInfo = '(see agent code)'; console.log(`Starting dev server...`); console.log(`Agent: ${config.agentName}`); if (config.protocol !== 'MCP') { console.log(`Provider: ${providerInfo}`); } if (config.protocol !== 'HTTP') { console.log(`Protocol: ${config.protocol}`); } console.log(`Server: ${getEndpointUrl(actualPort, config.protocol)}`); console.log(`Log: ${logger.getRelativeLogPath()}`); console.log(`Press Ctrl+C to stop\n`); const devCallbacks = { onLog: (level: string, msg: string) => { const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : '→'; console.log(`${prefix} ${msg}`); logger.log(msg, level === 'error' ? 'error' : 'info'); }, onExit: (code: number | null) => { console.log(`\nServer exited with code ${code ?? 0}`); logger.finalize(code === 0); process.exit(code ?? 0); }, }; const server = createDevServer(config, { port: actualPort, envVars: mergedEnvVars, callbacks: devCallbacks }); await server.start(); // Handle Ctrl+C — use server.kill() for proper container cleanup process.on('SIGINT', () => { console.log('\nStopping server...'); collector?.stop(); server.kill(); }); // Keep process alive // eslint-disable-next-line @typescript-eslint/no-empty-function await new Promise(() => {}); } // If --no-browser provided, launch terminal TUI mode if (!opts.browser) { // Enter alternate screen buffer for fullscreen mode process.stdout.write(ENTER_ALT_SCREEN); const exitAltScreen = () => { process.stdout.write(EXIT_ALT_SCREEN); process.stdout.write(SHOW_CURSOR); }; const { DevScreen } = await import('../../tui/screens/dev/DevScreen'); const { unmount, waitUntilExit } = render( { exitAltScreen(); unmount(); process.exit(0); }} workingDir={workingDir} port={port} agentName={opts.runtime} headers={headers} /> ); await waitUntilExit(); exitAltScreen(); return; } // Default: launch web UI in browser await runBrowserMode({ workingDir, project, port, agentName: opts.runtime, otelEnvVars, collector, }); } catch (error) { render(Error: {getErrorMessage(error)}); process.exit(1); } }); };