jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
316 lines (266 loc) • 8.89 kB
text/typescript
import * as process from 'node:process';
/**
* Terminal manager interface and implementation
*/
import type { AgentProfile, AgentSession, TerminalConfig } from '../utils/types.js';
import type { IEventBus } from '../core/event-bus.js';
import type { ILogger } from '../core/logger.js';
import { TerminalError, TerminalSpawnError } from '../utils/errors.js';
import type { ITerminalAdapter } from './adapters/base.js';
import { VSCodeAdapter } from './adapters/vscode.js';
import { NativeAdapter } from './adapters/native.js';
import { TerminalPool } from './pool.js';
import { TerminalSession } from './session.js';
export interface ITerminalManager {
initialize(): Promise<void>;
shutdown(): Promise<void>;
spawnTerminal(profile: AgentProfile): Promise<string>;
terminateTerminal(terminalId: string): Promise<void>;
executeCommand(terminalId: string, command: string): Promise<string>;
getHealthStatus(): Promise<{
healthy: boolean;
error?: string;
metrics?: Record<string, number>;
}>;
performMaintenance(): Promise<void>;
}
/**
* Terminal manager implementation
*/
export class TerminalManager implements ITerminalManager {
private adapter: ITerminalAdapter;
private pool: TerminalPool;
private sessions = new Map<string, TerminalSession>();
private initialized = false;
constructor(
private config: TerminalConfig,
private eventBus: IEventBus,
private logger: ILogger,
) {
// Select adapter based on configuration
this.adapter = this.createAdapter();
// Create terminal pool
this.pool = new TerminalPool(
this.config.poolSize,
this.config.recycleAfter,
this.adapter,
this.logger,
);
}
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
this.logger.info('Initializing terminal manager...');
try {
// Initialize adapter
await this.adapter.initialize();
// Initialize pool
await this.pool.initialize();
this.initialized = true;
this.logger.info('Terminal manager initialized');
} catch (error) {
this.logger.error('Failed to initialize terminal manager', error);
throw new TerminalError('Terminal manager initialization failed', { error });
}
}
async shutdown(): Promise<void> {
if (!this.initialized) {
return;
}
this.logger.info('Shutting down terminal manager...');
try {
// Terminate all sessions
const sessionIds = Array.from(this.sessions.keys());
await Promise.all(sessionIds.map((id) => this.terminateTerminal(id)));
// Shutdown pool
await this.pool.shutdown();
// Shutdown adapter
await this.adapter.shutdown();
this.initialized = false;
this.logger.info('Terminal manager shutdown complete');
} catch (error) {
this.logger.error('Error during terminal manager shutdown', error);
throw error;
}
}
async spawnTerminal(profile: AgentProfile): Promise<string> {
if (!this.initialized) {
throw new TerminalError('Terminal manager not initialized');
}
this.logger.debug('Spawning terminal', { agentId: profile.id });
try {
// Get terminal from pool
const terminal = await this.pool.acquire();
// Create session
const session = new TerminalSession(
terminal,
profile,
this.config.commandTimeout,
this.logger,
);
// Initialize session
await session.initialize();
// Store session
this.sessions.set(session.id, session);
this.logger.info('Terminal spawned', {
terminalId: session.id,
agentId: profile.id,
});
return session.id;
} catch (error) {
this.logger.error('Failed to spawn terminal', error);
throw new TerminalSpawnError('Failed to spawn terminal', { error });
}
}
async terminateTerminal(terminalId: string): Promise<void> {
const session = this.sessions.get(terminalId);
if (!session) {
throw new TerminalError(`Terminal not found: ${terminalId}`);
}
this.logger.debug('Terminating terminal', { terminalId });
try {
// Cleanup session
await session.cleanup();
// Return terminal to pool
await this.pool.release(session.terminal);
// Remove session
this.sessions.delete(terminalId);
this.logger.info('Terminal terminated', { terminalId });
} catch (error) {
this.logger.error('Failed to terminate terminal', error);
throw error;
}
}
async executeCommand(terminalId: string, command: string): Promise<string> {
const session = this.sessions.get(terminalId);
if (!session) {
throw new TerminalError(`Terminal not found: ${terminalId}`);
}
return await session.executeCommand(command);
}
async getHealthStatus(): Promise<{
healthy: boolean;
error?: string;
metrics?: Record<string, number>;
}> {
try {
const poolHealth = await this.pool.getHealthStatus();
const activeSessions = this.sessions.size;
const healthySessions = Array.from(this.sessions.values()).filter((session) =>
session.isHealthy(),
).length;
const metrics = {
activeSessions,
healthySessions,
poolSize: poolHealth.size,
availableTerminals: poolHealth.available,
recycledTerminals: poolHealth.recycled,
};
const healthy = poolHealth.healthy && healthySessions === activeSessions;
if (healthy) {
return {
healthy,
metrics,
};
} else {
return {
healthy,
metrics,
error: 'Some terminals are unhealthy',
};
}
} catch (error) {
return {
healthy: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async performMaintenance(): Promise<void> {
if (!this.initialized) {
return;
}
this.logger.debug('Performing terminal manager maintenance');
try {
// Clean up dead sessions
const deadSessions = Array.from(this.sessions.entries()).filter(
([_, session]) => !session.isHealthy(),
);
for (const [terminalId, _] of deadSessions) {
this.logger.warn('Cleaning up dead terminal session', { terminalId });
await this.terminateTerminal(terminalId).catch((error) =>
this.logger.error('Failed to clean up terminal', { terminalId, error }),
);
}
// Perform pool maintenance
await this.pool.performMaintenance();
// Emit maintenance event
this.eventBus.emit('terminal:maintenance', {
deadSessions: deadSessions.length,
activeSessions: this.sessions.size,
poolStatus: await this.pool.getHealthStatus(),
});
this.logger.debug('Terminal manager maintenance completed');
} catch (error) {
this.logger.error('Error during terminal manager maintenance', error);
}
}
/**
* Get all active sessions
*/
getActiveSessions(): AgentSession[] {
return Array.from(this.sessions.values()).map((session) => ({
id: session.id,
agentId: session.profile.id,
terminalId: session.terminal.id,
startTime: session.startTime,
status: session.isHealthy() ? 'active' : 'error',
lastActivity: session.lastActivity,
memoryBankId: '', // TODO: Link to memory bank
}));
}
/**
* Get session by ID
*/
getSession(sessionId: string): TerminalSession | undefined {
return this.sessions.get(sessionId);
}
/**
* Stream terminal output
*/
async streamOutput(terminalId: string, callback: (output: string) => void): Promise<() => void> {
const session = this.sessions.get(terminalId);
if (!session) {
throw new TerminalError(`Terminal not found: ${terminalId}`);
}
return session.streamOutput(callback);
}
private createAdapter(): ITerminalAdapter {
switch (this.config.type) {
case 'vscode':
return new VSCodeAdapter(this.logger);
case 'native':
return new NativeAdapter(this.logger);
case 'auto':
// Detect environment and choose appropriate adapter
if (this.isVSCodeEnvironment()) {
this.logger.info('Detected VSCode environment, using VSCode adapter');
return new VSCodeAdapter(this.logger);
} else {
this.logger.info('Using native terminal adapter');
return new NativeAdapter(this.logger);
}
default:
throw new TerminalError(`Unknown terminal type: ${this.config.type}`);
}
}
private isVSCodeEnvironment(): boolean {
// Check for VSCode-specific environment variables
return (
process.env.TERM_PROGRAM === 'vscode' ||
process.env.VSCODE_PID !== undefined ||
process.env.VSCODE_IPC_HOOK !== undefined
);
}
}