/**
* Detector for interactive terminal programs that require direct input
*/
export class InteractiveModeDetector {
// Common interactive programs that need direct input
private static readonly INTERACTIVE_PROGRAMS = [
'nano', 'vim', 'vi', 'emacs', 'pico', 'joe', 'jed',
'less', 'more', 'man',
'top', 'htop', 'iotop', 'iftop', 'nethogs',
'mysql', 'psql', 'sqlite3', 'mongo',
'python', 'python3', 'node', 'irb', 'php -a',
'ssh', 'telnet', 'ftp', 'sftp',
'screen', 'tmux',
'gdb', 'pdb', 'lldb',
'crontab -e', 'visudo',
'apt-get install', 'yum install', 'pacman',
'passwd', 'su', 'sudo -S'
];
// Terminal control sequences that indicate interactive mode
private static readonly INTERACTIVE_SEQUENCES = [
/\x1b\[2J/, // Clear screen
/\x1b\[H/, // Move cursor home
/\x1b\[\d+;\d+H/, // Move cursor to position
/\x1b\[\?1049h/, // Alternative screen buffer (used by vim, less, etc)
/\x1b\[6n/, // Request cursor position
/\x1b\[\d+m/, // Set graphics mode
];
private interactiveSessions: Set<string> = new Set();
private sessionCommands: Map<string, string> = new Map();
/**
* Check if a command will launch an interactive program
*/
isInteractiveCommand(command: string): boolean {
const cmdLower = command.toLowerCase().trim();
// Check for exact matches or command starts with interactive program
return InteractiveModeDetector.INTERACTIVE_PROGRAMS.some(prog => {
return cmdLower === prog ||
cmdLower.startsWith(prog + ' ') ||
cmdLower.includes('/' + prog + ' ') ||
cmdLower.endsWith('/' + prog);
});
}
/**
* Detect interactive mode from output
*/
detectInteractiveMode(sessionId: string, output: string): boolean {
// Check for terminal control sequences
const hasControlSequences = InteractiveModeDetector.INTERACTIVE_SEQUENCES.some(
pattern => pattern.test(output)
);
if (hasControlSequences) {
this.interactiveSessions.add(sessionId);
return true;
}
// Check for common interactive prompts
const interactivePrompts = [
/\(END\)$/, // less/more
/^:/, // vi command mode
/\[yes\/no\]/i, // confirmation prompts
/password:/i, // password prompts
/\(y\/n\)/i, // yes/no prompts
/Press any key/i, // pause prompts
/\x1b\[\d+;\d+r/, // Set scrolling region (used by editors)
];
const hasInteractivePrompt = interactivePrompts.some(
pattern => pattern.test(output)
);
if (hasInteractivePrompt) {
this.interactiveSessions.add(sessionId);
return true;
}
return false;
}
/**
* Track command for session
*/
trackCommand(sessionId: string, command: string): void {
this.sessionCommands.set(sessionId, command);
// If it's an interactive command, mark session as interactive
if (this.isInteractiveCommand(command)) {
this.interactiveSessions.add(sessionId);
}
}
/**
* Check if session is currently in interactive mode
*/
isInteractive(sessionId: string): boolean {
return this.interactiveSessions.has(sessionId);
}
/**
* Clear interactive mode for session (e.g., when program exits)
*/
clearInteractiveMode(sessionId: string): void {
this.interactiveSessions.delete(sessionId);
this.sessionCommands.delete(sessionId);
}
/**
* Detect when interactive program has exited
*/
detectInteractiveExit(sessionId: string, output: string): boolean {
// Common patterns that indicate return to shell prompt
const exitPatterns = [
/\$ $/, // Bash prompt
/# $/, // Root prompt
/> $/, // Generic prompt
/\x1b\[\?1049l/, // Exit alternative screen buffer
/logout|exit|bye/i, // Exit commands
/Process exited/i,
];
const hasExited = exitPatterns.some(pattern => pattern.test(output));
if (hasExited && this.interactiveSessions.has(sessionId)) {
this.interactiveSessions.delete(sessionId);
return true;
}
return false;
}
/**
* Get interactive status for all sessions
*/
getInteractiveSessions(): string[] {
return Array.from(this.interactiveSessions);
}
}