#!/usr/bin/env python3 """ Full-featured Terminal Emulator for HenAi Supports Windows, Linux, and macOS with real system command execution Can be imported and used within Flask applications """ import os import sys import subprocess import shutil import signal import threading import queue import time import platform import re import glob import stat import hashlib import tempfile import zipfile import tarfile import json import csv import pickle import sqlite3 import argparse import logging import configparser import urllib.request import urllib.parse import socket import psutil from pathlib import Path from datetime import datetime, timedelta from typing import Optional, List, Tuple, Dict, Any, Callable, Union from dataclasses import dataclass, field from enum import Enum import warnings # Platform detection IS_WINDOWS = platform.system() == "Windows" IS_LINUX = platform.system() == "Linux" IS_MAC = platform.system() == "Darwin" # ANSI color codes for cross-platform colored output class Colors: RESET = '\033[0m' BOLD = '\033[1m' DIM = '\033[2m' UNDERLINE = '\033[4m' BLINK = '\033[5m' REVERSE = '\033[7m' HIDDEN = '\033[8m' # Foreground colors BLACK = '\033[30m' RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' BLUE = '\033[94m' MAGENTA = '\033[95m' CYAN = '\033[96m' WHITE = '\033[97m' GRAY = '\033[90m' # Background colors BG_BLACK = '\033[40m' BG_RED = '\033[101m' BG_GREEN = '\033[102m' BG_YELLOW = '\033[103m' BG_BLUE = '\033[104m' BG_MAGENTA = '\033[105m' BG_CYAN = '\033[106m' BG_WHITE = '\033[107m' @staticmethod def disable(): """Disable colored output for Windows compatibility""" if IS_WINDOWS: return True return False class CommandType(Enum): BUILTIN = "builtin" SYSTEM = "system" ALIAS = "alias" FUNCTION = "function" @dataclass class CommandResult: """Result of command execution""" success: bool output: str error: str = "" exit_code: int = 0 execution_time: float = 0.0 command: str = "" @dataclass class ProcessInfo: """Information about a running process""" pid: int name: str status: str cpu_percent: float memory_percent: float memory_rss: int memory_vms: int create_time: float username: str cmdline: List[str] parent_pid: Optional[int] = None class HistoryManager: """Manages command history with persistence""" def __init__(self, history_file: str = None): if not history_file: history_file = os.path.expanduser("~/.terminal_history") self.history_file = history_file self.history: List[str] = [] self.current_index: int = -1 self.load_history() def load_history(self): """Load command history from file""" try: if os.path.exists(self.history_file): with open(self.history_file, 'r', encoding='utf-8') as f: self.history = [line.strip() for line in f if line.strip()] except Exception: self.history = [] self.current_index = len(self.history) def save_history(self): """Save command history to file""" try: with open(self.history_file, 'w', encoding='utf-8') as f: for cmd in self.history[-1000:]: # Keep last 1000 commands f.write(cmd + '\n') except Exception: pass def add_command(self, command: str): """Add command to history""" if not command or command.strip() == "": return if self.history and self.history[-1] == command: return self.history.append(command) self.current_index = len(self.history) self.save_history() def get_previous(self) -> Optional[str]: """Get previous command in history""" if self.current_index > 0: self.current_index -= 1 return self.history[self.current_index] return None def get_next(self) -> Optional[str]: """Get next command in history""" if self.current_index < len(self.history) - 1: self.current_index += 1 return self.history[self.current_index] self.current_index = len(self.history) return None def search(self, pattern: str) -> List[str]: """Search history for commands matching pattern""" return [cmd for cmd in self.history if pattern in cmd] def clear(self): """Clear command history""" self.history = [] self.current_index = 0 self.save_history() class AliasManager: """Manages command aliases""" def __init__(self, alias_file: str = None): if not alias_file: alias_file = os.path.expanduser("~/.terminal_aliases") self.alias_file = alias_file self.aliases: Dict[str, str] = {} self.load_aliases() def load_aliases(self): """Load aliases from file""" try: if os.path.exists(self.alias_file): with open(self.alias_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line and '=' in line and not line.startswith('#'): name, cmd = line.split('=', 1) self.aliases[name.strip()] = cmd.strip() except Exception: pass def save_aliases(self): """Save aliases to file""" try: with open(self.alias_file, 'w', encoding='utf-8') as f: for name, cmd in self.aliases.items(): f.write(f"{name}={cmd}\n") except Exception: pass def add_alias(self, name: str, command: str) -> bool: """Add or update an alias""" if not name or not re.match(r'^[a-zA-Z0-9_\-]+$', name): return False self.aliases[name] = command self.save_aliases() return True def remove_alias(self, name: str) -> bool: """Remove an alias""" if name in self.aliases: del self.aliases[name] self.save_aliases() return True return False def get_alias(self, name: str) -> Optional[str]: """Get alias command""" return self.aliases.get(name) def expand(self, command: str) -> str: """Expand aliases in command""" parts = command.split() if not parts: return command if parts[0] in self.aliases: return self.aliases[parts[0]] + ' ' + ' '.join(parts[1:]) return command def list_aliases(self) -> Dict[str, str]: """List all aliases""" return self.aliases.copy() class EnvironmentManager: """Manages environment variables""" def __init__(self): self.env = os.environ.copy() self.custom_vars: Dict[str, str] = {} def get(self, key: str, default: str = None) -> Optional[str]: """Get environment variable""" if key in self.custom_vars: return self.custom_vars[key] return self.env.get(key, default) def set(self, key: str, value: str, permanent: bool = False): """Set environment variable""" if permanent: self.custom_vars[key] = value else: self.env[key] = value def unset(self, key: str): """Unset environment variable""" if key in self.custom_vars: del self.custom_vars[key] if key in self.env: del self.env[key] def get_all(self) -> Dict[str, str]: """Get all environment variables""" result = self.env.copy() result.update(self.custom_vars) return result def expand(self, value: str) -> str: """Expand environment variables in string""" return os.path.expandvars(value) class VirtualEnvironmentManager: """Manages Python virtual environments""" def __init__(self, terminal): self.terminal = terminal self.current_venv: Optional[str] = None self.current_venv_path: Optional[str] = None self.venv_python_path: Optional[str] = None self.venv_pip_path: Optional[str] = None def create_venv(self, name: str, path: str = None, python_version: str = None) -> CommandResult: """Create a new virtual environment""" if not path: path = os.getcwd() venv_path = os.path.join(path, name) if os.path.exists(venv_path): return CommandResult( success=False, output="", error=f"Virtual environment '{name}' already exists at {venv_path}", exit_code=1, command=f"venv create {name}" ) try: python_cmd = sys.executable if python_version: # Try to use specified Python version python_cmd = f"python{python_version}" result = subprocess.run( [python_cmd, "-m", "venv", venv_path], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: return CommandResult( success=True, output=f"Virtual environment '{name}' created successfully at {venv_path}\n\nTo activate: venv activate {name}", exit_code=0, command=f"venv create {name}" ) else: return CommandResult( success=False, output="", error=result.stderr, exit_code=result.returncode, command=f"venv create {name}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Operation timed out", exit_code=1, command=f"venv create {name}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"venv create {name}" ) def activate_venv(self, name: str, path: str = None) -> CommandResult: """Activate a virtual environment""" if not path: path = os.getcwd() venv_path = os.path.join(path, name) if not os.path.exists(venv_path): return CommandResult( success=False, output="", error=f"Virtual environment '{name}' not found at {venv_path}", exit_code=1, command=f"venv activate {name}" ) if IS_WINDOWS: python_path = os.path.join(venv_path, "Scripts", "python.exe") pip_path = os.path.join(venv_path, "Scripts", "pip.exe") activate_script = os.path.join(venv_path, "Scripts", "activate") else: python_path = os.path.join(venv_path, "bin", "python") pip_path = os.path.join(venv_path, "bin", "pip") activate_script = os.path.join(venv_path, "bin", "activate") if os.path.exists(python_path): self.current_venv = name self.current_venv_path = venv_path self.venv_python_path = python_path self.venv_pip_path = pip_path output = f"Activated virtual environment: {name}\n" output += f"Python: {python_path}\n" output += f"Pip: {pip_path}\n\n" output += f"Run 'deactivate' to exit this environment." return CommandResult( success=True, output=output, exit_code=0, command=f"venv activate {name}" ) else: return CommandResult( success=False, output="", error=f"Python executable not found in virtual environment", exit_code=1, command=f"venv activate {name}" ) def deactivate_venv(self) -> CommandResult: """Deactivate current virtual environment""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is currently active", exit_code=1, command="venv deactivate" ) output = f"Deactivated virtual environment: {self.current_venv}" self.current_venv = None self.current_venv_path = None self.venv_python_path = None self.venv_pip_path = None return CommandResult( success=True, output=output, exit_code=0, command="venv deactivate" ) def list_venvs(self, path: str = None) -> CommandResult: """List all virtual environments in current directory""" if not path: path = os.getcwd() venvs = [] try: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isdir(item_path): if IS_WINDOWS: check_path = os.path.join(item_path, "Scripts", "python.exe") else: check_path = os.path.join(item_path, "bin", "python") if os.path.exists(check_path): venvs.append(item) except PermissionError: pass output = "Virtual environments found:\n" for venv in venvs: indicator = " [ACTIVE]" if venv == self.current_venv else "" output += f" - {venv}{indicator}\n" if not venvs: output = "No virtual environments found in current directory" return CommandResult( success=True, output=output, exit_code=0, command="venv list" ) def install_package(self, package: str, version: str = None) -> CommandResult: """Install a package in the active virtual environment""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is active. Use 'venv activate ' first.", exit_code=1, command=f"pip install {package}" ) pkg_spec = f"{package}=={version}" if version else package try: result = subprocess.run( [self.venv_python_path, "-m", "pip", "install", pkg_spec], capture_output=True, text=True, timeout=300 ) if result.returncode == 0: return CommandResult( success=True, output=result.stdout, exit_code=0, command=f"pip install {package}" ) else: return CommandResult( success=False, output="", error=result.stderr, exit_code=result.returncode, command=f"pip install {package}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Installation timed out", exit_code=1, command=f"pip install {package}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"pip install {package}" ) def uninstall_package(self, package: str) -> CommandResult: """Uninstall a package from the active virtual environment""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is active", exit_code=1, command=f"pip uninstall {package}" ) try: result = subprocess.run( [self.venv_python_path, "-m", "pip", "uninstall", package, "-y"], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: return CommandResult( success=True, output=result.stdout, exit_code=0, command=f"pip uninstall {package}" ) else: return CommandResult( success=False, output="", error=result.stderr, exit_code=result.returncode, command=f"pip uninstall {package}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"pip uninstall {package}" ) def list_packages(self) -> CommandResult: """List installed packages in active virtual environment""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is active", exit_code=1, command="pip list" ) try: result = subprocess.run( [self.venv_python_path, "-m", "pip", "list"], capture_output=True, text=True, timeout=30 ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command="pip list" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command="pip list" ) def freeze_requirements(self, output_file: str = "requirements.txt") -> CommandResult: """Generate requirements.txt from active virtual environment""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is active", exit_code=1, command=f"pip freeze > {output_file}" ) try: result = subprocess.run( [self.venv_python_path, "-m", "pip", "freeze"], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: with open(output_file, 'w', encoding='utf-8') as f: f.write(result.stdout) return CommandResult( success=True, output=f"Requirements saved to {output_file}\n\n{result.stdout[:500]}{'...' if len(result.stdout) > 500 else ''}", exit_code=0, command=f"pip freeze > {output_file}" ) else: return CommandResult( success=False, output="", error=result.stderr, exit_code=result.returncode, command=f"pip freeze > {output_file}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"pip freeze > {output_file}" ) def install_requirements(self, requirements_file: str = "requirements.txt") -> CommandResult: """Install packages from requirements.txt""" if not self.current_venv: return CommandResult( success=False, output="", error="No virtual environment is active", exit_code=1, command=f"pip install -r {requirements_file}" ) try: result = subprocess.run( [self.venv_python_path, "-m", "pip", "install", "-r", requirements_file], capture_output=True, text=True, timeout=300 ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command=f"pip install -r {requirements_file}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Installation timed out", exit_code=1, command=f"pip install -r {requirements_file}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"pip install -r {requirements_file}" ) def run_in_venv(self, command: List[str]) -> subprocess.Popen: """Run a command in the active virtual environment""" if self.current_venv: # Prepend venv python path if command starts with python if command and command[0] in ['python', 'python3']: command = [self.venv_python_path] + command[1:] return subprocess.Popen(command, shell=IS_WINDOWS) def get_venv_info(self) -> Dict[str, Any]: """Get information about current virtual environment""" if not self.current_venv: return {"active": False} return { "active": True, "name": self.current_venv, "path": self.current_venv_path, "python": self.venv_python_path, "pip": self.venv_pip_path } class FileSystemManager: """Manages file system operations""" def __init__(self, terminal): self.terminal = terminal def ls(self, args: List[str]) -> CommandResult: """List directory contents with PowerShell-style table format - workspace only""" try: workspace_root = getattr(self.terminal, 'workspace_root', os.path.abspath('.')) path = self.terminal.current_directory # Ensure path is within workspace try: if not path.startswith(workspace_root): path = workspace_root self.terminal.current_directory = workspace_root except (ValueError, AttributeError): path = workspace_root self.terminal.current_directory = workspace_root show_all = False long_format = False human_readable = False recursive = False for arg in args: if arg == '-a' or arg == '--all': show_all = True elif arg == '-l': long_format = True elif arg == '-h' or arg == '--human-readable': human_readable = True elif arg == '-R' or arg == '--recursive': recursive = True elif not arg.startswith('-'): # Handle relative paths within workspace target = arg # Remove any Windows drive letters target = re.sub(r'^[A-Za-z]:\\', '', target) target = target.replace('/', os.sep).replace('\\', os.sep) if os.path.isabs(target): target = os.path.normpath(target) else: target = os.path.join(self.terminal.current_directory, target) # Ensure within workspace try: if target.startswith(workspace_root): path = target else: return CommandResult( success=False, output="", error=f"Cannot access outside workspace: {arg}", exit_code=1, command=f"ls {' '.join(args)}" ) except (ValueError, AttributeError): path = workspace_root if not os.path.exists(path): return CommandResult( success=False, output="", error=f"Path not found: {path}", exit_code=1, command=f"ls {' '.join(args)}" ) if recursive: return self._ls_recursive(path, show_all, long_format, human_readable) items = os.listdir(path) if not show_all: items = [item for item in items if not item.startswith('.')] items.sort() # Always use table format (PowerShell style) output = self._format_powershell_table(path, items) return CommandResult( success=True, output=output, exit_code=0, command=f"ls {' '.join(args)}" ) except PermissionError: return CommandResult( success=False, output="", error="Permission denied", exit_code=1, command=f"ls {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"ls {' '.join(args)}" ) for arg in args: if arg == '-a' or arg == '--all': show_all = True elif arg == '-l': long_format = True elif arg == '-h' or arg == '--human-readable': human_readable = True elif arg == '-R' or arg == '--recursive': recursive = True elif not arg.startswith('-'): path = os.path.join(self.terminal.current_directory, arg) if not os.path.exists(path): return CommandResult( success=False, output="", error=f"Path not found: {path}", exit_code=1, command=f"ls {' '.join(args)}" ) if recursive: return self._ls_recursive(path, show_all, long_format, human_readable) items = os.listdir(path) if not show_all: items = [item for item in items if not item.startswith('.')] items.sort() # Always use table format (PowerShell style) output = self._format_powershell_table(path, items) return CommandResult( success=True, output=output, exit_code=0, command=f"ls {' '.join(args)}" ) except PermissionError: return CommandResult( success=False, output="", error="Permission denied", exit_code=1, command=f"ls {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"ls {' '.join(args)}" ) def _format_powershell_table(self, path: str, items: List[str]) -> str: """Format directory listing as PowerShell-style table""" if not items: return "Directory is empty." # Collect file information file_infos = [] max_name_len = 4 # "Name" minimum max_mode_len = 6 # "Mode" minimum max_size_len = 6 # "Length" minimum for item in items: item_path = os.path.join(path, item) try: stat_info = os.stat(item_path) is_dir = os.path.isdir(item_path) is_symlink = os.path.islink(item_path) # Mode string (d----- for directory, -a---- for file) if is_dir: mode = "d-----" elif is_symlink: mode = "l-----" else: # Check if executable is_exec = os.access(item_path, os.X_OK) mode = "-a----" if is_exec else "------" # Size size = stat_info.st_size size_str = f"{size:,}" if size > 0 else "0" # Last write time mtime = datetime.fromtimestamp(stat_info.st_mtime) time_str = mtime.strftime("%m/%d/%Y %I:%M %p").lstrip('0').replace(' 0', ' ') name = item + ("/" if is_dir else "") file_infos.append({ 'mode': mode, 'size': size_str, 'size_bytes': size, 'time': time_str, 'name': name, 'is_dir': is_dir }) max_name_len = max(max_name_len, len(name)) max_mode_len = max(max_mode_len, len(mode)) max_size_len = max(max_size_len, len(size_str)) except (PermissionError, OSError): file_infos.append({ 'mode': "?-----", 'size': "?", 'size_bytes': 0, 'time': "?", 'name': item, 'is_dir': False }) # Build the table header separator = "-" * (max_mode_len + max_size_len + max_name_len + 20) output = [] output.append("") output.append(f" Directory: {self._get_friendly_path(path)}") output.append("") output.append(f"{'Mode':<{max_mode_len}} {'LastWriteTime':<20} {'Length':<{max_size_len}} Name") output.append(f"{'-' * max_mode_len:<{max_mode_len}} {'-' * 20:<20} {'-' * max_size_len:<{max_size_len}} {'-' * max_name_len}") # Sort: directories first, then files file_infos.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) for info in file_infos: output.append(f"{info['mode']:<{max_mode_len}} {info['time']:<20} {info['size']:>{max_size_len}} {info['name']}") return '\n'.join(output) def _get_friendly_path(self, path: str) -> str: """Convert system path to friendly HenAi path format - workspace only""" # Force everything to be relative to workspace root workspace_root = getattr(self.terminal, 'workspace_root', os.path.abspath('.')) try: # Get path relative to workspace root rel_path = os.path.relpath(path, workspace_root) # Handle special cases if rel_path == '.': return "HenAi:\\Workspace" elif rel_path.startswith('..'): # This should never happen - we shouldn't allow navigation outside workspace # But if it does, just show as Workspace return "HenAi:\\Workspace" else: # Convert to Windows-style path for display display_path = rel_path.replace('/', '\\') return f"HenAi:\\Workspace\\{display_path}" except ValueError: # If paths are on different drives, just show Workspace return "HenAi:\\Workspace" def _ls_recursive(self, path: str, show_all: bool, long_format: bool, human_readable: bool) -> CommandResult: """Recursive directory listing""" output = [] def walk_dir(current_path: str, indent: str = ""): try: items = os.listdir(current_path) if not show_all: items = [item for item in items if not item.startswith('.')] items.sort() rel_path = os.path.relpath(current_path, path) if rel_path == '.': output.append(f"\n{current_path}:") else: output.append(f"\n{current_path}:") for item in items: item_path = os.path.join(current_path, item) if long_format: output.append(self._get_file_info_line(item_path, human_readable)) else: if os.path.isdir(item_path): output.append(f"{Colors.BLUE}{item}/{Colors.RESET}") elif os.access(item_path, os.X_OK): output.append(f"{Colors.GREEN}{item}{Colors.RESET}") else: output.append(item) for item in items: item_path = os.path.join(current_path, item) if os.path.isdir(item_path): walk_dir(item_path, indent + " ") except PermissionError: output.append(f"{indent}[Permission Denied]") except Exception: pass walk_dir(path) return CommandResult( success=True, output='\n'.join(output), exit_code=0, command="ls -R" ) def _format_short_listing(self, path: str, items: List[str]) -> str: """Format short directory listing""" columns = 4 col_width = max(len(item) for item in items) + 2 if items else 10 rows = (len(items) + columns - 1) // columns output_lines = [] for row in range(rows): line = "" for col in range(columns): idx = row + col * rows if idx < len(items): item = items[idx] item_path = os.path.join(path, item) if os.path.isdir(item_path): line += f"{Colors.BLUE}{item}{Colors.RESET}".ljust(col_width) elif os.access(item_path, os.X_OK): line += f"{Colors.GREEN}{item}{Colors.RESET}".ljust(col_width) else: line += item.ljust(col_width) output_lines.append(line.rstrip()) return '\n'.join(output_lines) def _format_long_listing(self, path: str, items: List[str], human_readable: bool) -> str: """Format long directory listing (like ls -l)""" output = [] for item in items: item_path = os.path.join(path, item) output.append(self._get_file_info_line(item_path, human_readable)) return '\n'.join(output) def _get_file_info_line(self, file_path: str, human_readable: bool) -> str: """Get detailed file information line""" try: stat_info = os.stat(file_path) # File type and permissions mode = stat_info.st_mode file_type = 'd' if os.path.isdir(file_path) else '-' perms = '' for i, who in enumerate([stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR, stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH]): perms += 'r' if mode & who and i % 3 == 0 else '' perms += 'w' if mode & who and i % 3 == 1 else '' perms += 'x' if mode & who and i % 3 == 2 else '-' # Number of hard links nlink = stat_info.st_nlink # Owner and group try: import pwd, grp owner = pwd.getpwuid(stat_info.st_uid).pw_name group = grp.getgrgid(stat_info.st_gid).gr_name except: owner = str(stat_info.st_uid) group = str(stat_info.st_gid) # Size size = stat_info.st_size if human_readable: size = self._human_readable_size(size) else: size = str(size) # Modification time mtime = datetime.fromtimestamp(stat_info.st_mtime).strftime("%b %d %H:%M") # Name name = os.path.basename(file_path) if os.path.isdir(file_path): name = f"{Colors.BLUE}{name}/{Colors.RESET}" elif os.access(file_path, os.X_OK): name = f"{Colors.GREEN}{name}{Colors.RESET}" return f"{file_type}{perms} {nlink:2} {owner} {group} {size:>8} {mtime} {name}" except Exception: return f"[Error] {os.path.basename(file_path)}" def _human_readable_size(self, size: int) -> str: """Convert size to human readable format""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if size < 1024.0: return f"{size:.1f}{unit}" size /= 1024.0 return f"{size:.1f}PB" def cd(self, args: List[str]) -> CommandResult: """Change directory - STRICTLY within workspace only""" workspace_root = getattr(self.terminal, 'workspace_root', os.path.abspath('.')) if not args: # Go to workspace root new_dir = workspace_root else: new_dir = args[0] # Handle special paths if new_dir == '~' or new_dir == '~\\' or new_dir == '/' or new_dir == '\\': new_dir = workspace_root elif new_dir == '..': # Go up one level, but never above workspace root current = self.terminal.current_directory parent = os.path.dirname(current) if os.path.commonpath([parent, workspace_root]) == workspace_root: new_dir = parent else: new_dir = workspace_root else: # Remove any Windows drive letters or backslashes new_dir = new_dir.replace('C:\\', '').replace('D:\\', '').replace(':\\', '') new_dir = new_dir.replace('/', os.sep).replace('\\', os.sep) # Build absolute path within workspace if os.path.isabs(new_dir): # If it's absolute, make it relative to workspace root new_dir = os.path.normpath(new_dir) else: new_dir = os.path.join(self.terminal.current_directory, new_dir) new_dir = os.path.normpath(new_dir) # CRITICAL: Never allow navigation outside workspace root try: if not new_dir.startswith(workspace_root): new_dir = workspace_root except (ValueError, AttributeError): new_dir = workspace_root if os.path.exists(new_dir) and os.path.isdir(new_dir): self.terminal.current_directory = new_dir os.chdir(new_dir) friendly_path = self._get_friendly_path(new_dir) return CommandResult( success=True, output=friendly_path, exit_code=0, command=f"cd {' '.join(args)}" ) else: return CommandResult( success=False, output="", error=f"Directory not found: {new_dir}", exit_code=1, command=f"cd {' '.join(args)}" ) def pwd(self, args: List[str]) -> CommandResult: """Print working directory (friendly format)""" friendly_path = self._get_friendly_path(self.terminal.current_directory) return CommandResult( success=True, output=friendly_path, exit_code=0, command="pwd" ) def mkdir(self, args: List[str]) -> CommandResult: """Create directories""" if not args: return CommandResult( success=False, output="", error="Usage: mkdir [-p] ...", exit_code=1, command="mkdir" ) parents = '-p' in args or '--parents' in args args = [arg for arg in args if not arg.startswith('-')] output = [] errors = [] for dirname in args: try: dirpath = os.path.join(self.terminal.current_directory, dirname) if parents: os.makedirs(dirpath, exist_ok=True) else: os.mkdir(dirpath) output.append(f"Created directory: {dirname}") except FileExistsError: errors.append(f"Directory already exists: {dirname}") except Exception as e: errors.append(f"Failed to create {dirname}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"mkdir {' '.join(args)}" ) def rmdir(self, args: List[str]) -> CommandResult: """Remove empty directories""" if not args: return CommandResult( success=False, output="", error="Usage: rmdir ...", exit_code=1, command="rmdir" ) output = [] errors = [] for dirname in args: try: os.rmdir(os.path.join(self.terminal.current_directory, dirname)) output.append(f"Removed directory: {dirname}") except OSError as e: errors.append(f"Failed to remove {dirname}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"rmdir {' '.join(args)}" ) def rm(self, args: List[str]) -> CommandResult: """Remove files or directories""" if not args: return CommandResult( success=False, output="", error="Usage: rm [-r] [-f] ...", exit_code=1, command="rm" ) recursive = '-r' in args or '-rf' in args force = '-f' in args or '-rf' in args args = [arg for arg in args if not arg.startswith('-')] output = [] errors = [] for filename in args: try: path = os.path.join(self.terminal.current_directory, filename) if os.path.isdir(path): if recursive: shutil.rmtree(path) output.append(f"Removed directory: {filename}") else: errors.append(f"Cannot remove directory '{filename}'. Use -r flag") else: os.remove(path) output.append(f"Removed file: {filename}") except FileNotFoundError: if not force: errors.append(f"File not found: {filename}") except Exception as e: if not force: errors.append(f"Failed to remove {filename}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"rm {' '.join(args)}" ) def cp(self, args: List[str]) -> CommandResult: """Copy files or directories""" if len(args) < 2: return CommandResult( success=False, output="", error="Usage: cp [-r] ", exit_code=1, command="cp" ) recursive = '-r' in args args = [arg for arg in args if not arg.startswith('-')] if len(args) < 2: return CommandResult( success=False, output="", error="Usage: cp [-r] ", exit_code=1, command="cp" ) src = os.path.join(self.terminal.current_directory, args[0]) dst = os.path.join(self.terminal.current_directory, args[1]) try: if os.path.isdir(src): if recursive: shutil.copytree(src, dst) else: return CommandResult( success=False, output="", error=f"Source is a directory. Use -r flag to copy directories.", exit_code=1, command=f"cp {' '.join(args)}" ) else: shutil.copy2(src, dst) return CommandResult( success=True, output=f"Copied {args[0]} to {args[1]}", exit_code=0, command=f"cp {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"cp {' '.join(args)}" ) def mv(self, args: List[str]) -> CommandResult: """Move or rename files or directories""" if len(args) < 2: return CommandResult( success=False, output="", error="Usage: mv ", exit_code=1, command="mv" ) src = os.path.join(self.terminal.current_directory, args[0]) dst = os.path.join(self.terminal.current_directory, args[1]) try: shutil.move(src, dst) return CommandResult( success=True, output=f"Moved {args[0]} to {args[1]}", exit_code=0, command=f"mv {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"mv {' '.join(args)}" ) def touch(self, args: List[str]) -> CommandResult: """Create empty files or update timestamps""" if not args: return CommandResult( success=False, output="", error="Usage: touch ...", exit_code=1, command="touch" ) output = [] errors = [] for filename in args: try: filepath = os.path.join(self.terminal.current_directory, filename) with open(filepath, 'a'): os.utime(filepath, None) output.append(f"Created/updated: {filename}") except Exception as e: errors.append(f"Failed: {filename} - {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"touch {' '.join(args)}" ) def cat(self, args: List[str]) -> CommandResult: """Concatenate and display files""" if not args: return CommandResult( success=False, output="", error="Usage: cat ...", exit_code=1, command="cat" ) output = [] errors = [] for filename in args: try: with open(os.path.join(self.terminal.current_directory, filename), 'r', encoding='utf-8', errors='ignore') as f: output.append(f.read()) except Exception as e: errors.append(f"Failed to read {filename}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"cat {' '.join(args)}" ) def head(self, args: List[str]) -> CommandResult: """Display first lines of files""" n = 10 if args and args[0].startswith('-n'): try: n = int(args[0][2:]) if len(args[0]) > 2 else int(args[1]) args = args[2:] if len(args[0]) == 2 else args[1:] except: pass if not args: return CommandResult( success=False, output="", error="Usage: head [-n N] ", exit_code=1, command="head" ) output = [] errors = [] for filename in args: try: with open(os.path.join(self.terminal.current_directory, filename), 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines()[:n] output.append(f"==> {filename} <==\n" + ''.join(lines)) except Exception as e: errors.append(f"Failed to read {filename}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"head {' '.join(args)}" ) def tail(self, args: List[str]) -> CommandResult: """Display last lines of files""" n = 10 if args and args[0].startswith('-n'): try: n = int(args[0][2:]) if len(args[0]) > 2 else int(args[1]) args = args[2:] if len(args[0]) == 2 else args[1:] except: pass if not args: return CommandResult( success=False, output="", error="Usage: tail [-n N] ", exit_code=1, command="tail" ) output = [] errors = [] for filename in args: try: with open(os.path.join(self.terminal.current_directory, filename), 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines()[-n:] output.append(f"==> {filename} <==\n" + ''.join(lines)) except Exception as e: errors.append(f"Failed to read {filename}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"tail {' '.join(args)}" ) def find(self, args: List[str]) -> CommandResult: """Search for files""" search_name = None search_type = None search_path = self.terminal.current_directory i = 0 while i < len(args): if args[i] == '-name' and i + 1 < len(args): search_name = args[i + 1] i += 2 elif args[i] == '-type' and i + 1 < len(args): search_type = args[i + 1] i += 2 elif args[i] == '-path' and i + 1 < len(args): search_path = os.path.join(self.terminal.current_directory, args[i + 1]) i += 2 else: i += 1 if not search_name: return CommandResult( success=False, output="", error="Usage: find [-path PATH] -name PATTERN [-type f|d]", exit_code=1, command="find" ) found = [] pattern = search_name.replace('*', '.*').replace('?', '.') for root, dirs, files in os.walk(search_path): try: for name in files + dirs: if re.match(pattern, name, re.IGNORECASE): item_path = os.path.join(root, name) if search_type: if search_type == 'f' and os.path.isfile(item_path): found.append(os.path.relpath(item_path, self.terminal.current_directory)) elif search_type == 'd' and os.path.isdir(item_path): found.append(os.path.relpath(item_path, self.terminal.current_directory)) else: found.append(os.path.relpath(item_path, self.terminal.current_directory)) except PermissionError: continue output = '\n'.join(found) if found else f"No files found matching '{search_name}'" return CommandResult( success=True, output=output, exit_code=0, command=f"find {' '.join(args)}" ) def grep(self, args: List[str]) -> CommandResult: """Search for patterns in files""" if len(args) < 2: return CommandResult( success=False, output="", error="Usage: grep [-i] PATTERN [FILE]", exit_code=1, command="grep" ) ignore_case = '-i' in args args = [arg for arg in args if arg != '-i'] pattern = args[0] filename = args[1] if len(args) > 1 else None if ignore_case: pattern = pattern.lower() if filename: try: with open(os.path.join(self.terminal.current_directory, filename), 'r', encoding='utf-8', errors='ignore') as f: output = [] for line_num, line in enumerate(f, 1): search_line = line.lower() if ignore_case else line if pattern in search_line: output.append(f"{line_num}: {line.rstrip()}") if not output: output = [f"No matches found for '{args[0]}'"] return CommandResult( success=True, output='\n'.join(output), exit_code=0, command=f"grep {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"grep {' '.join(args)}" ) else: return CommandResult( success=False, output="", error="No file specified", exit_code=1, command=f"grep {' '.join(args)}" ) def tree(self, args: List[str]) -> CommandResult: """Display directory tree structure""" path = self.terminal.current_directory max_depth = None show_hidden = False for arg in args: if arg == '-a': show_hidden = True elif arg.startswith('-L') and len(arg) > 2: try: max_depth = int(arg[2:]) except: pass output = [os.path.basename(path) or path] def build_tree(current_path: str, prefix: str = "", depth: int = 0): if max_depth is not None and depth >= max_depth: return try: items = sorted(os.listdir(current_path)) if not show_hidden: items = [item for item in items if not item.startswith('.')] for i, item in enumerate(items): item_path = os.path.join(current_path, item) is_last = i == len(items) - 1 connector = "└── " if is_last else "├── " output.append(f"{prefix}{connector}{item}") if os.path.isdir(item_path): extension = " " if is_last else "│ " build_tree(item_path, prefix + extension, depth + 1) except PermissionError: output.append(f"{prefix}[Permission Denied]") build_tree(path) return CommandResult( success=True, output='\n'.join(output), exit_code=0, command=f"tree {' '.join(args)}" ) def du(self, args: List[str]) -> CommandResult: """Display disk usage""" path = self.terminal.current_directory human_readable = '-h' in args summarize = '-s' in args try: total_size = 0 if summarize: for root, dirs, files in os.walk(path): for file in files: try: total_size += os.path.getsize(os.path.join(root, file)) except: pass size_str = self._human_readable_size(total_size) if human_readable else str(total_size) output = f"{size_str}\t{os.path.basename(path) or path}" else: output = [] for item in os.listdir(path): item_path = os.path.join(path, item) try: if os.path.isfile(item_path): size = os.path.getsize(item_path) size_str = self._human_readable_size(size) if human_readable else str(size) output.append(f"{size_str}\t{item}") elif os.path.isdir(item_path): dir_size = 0 for root, dirs, files in os.walk(item_path): for file in files: try: dir_size += os.path.getsize(os.path.join(root, file)) except: pass size_str = self._human_readable_size(dir_size) if human_readable else str(dir_size) output.append(f"{size_str}\t{item}/") except: output.append(f"?\t{item}") output = '\n'.join(output) return CommandResult( success=True, output=output, exit_code=0, command=f"du {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"du {' '.join(args)}" ) def zip_files(self, args: List[str]) -> CommandResult: """Create ZIP archive""" if len(args) < 2: return CommandResult( success=False, output="", error="Usage: zip [file2 ...]", exit_code=1, command="zip" ) archive_name = args[0] if not archive_name.endswith('.zip'): archive_name += '.zip' archive_path = os.path.join(self.terminal.current_directory, archive_name) files = args[1:] try: with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for file in files: file_path = os.path.join(self.terminal.current_directory, file) if os.path.exists(file_path): if os.path.isfile(file_path): zipf.write(file_path, file) elif os.path.isdir(file_path): for root, dirs, files_in_dir in os.walk(file_path): for f in files_in_dir: full_path = os.path.join(root, f) arcname = os.path.relpath(full_path, self.terminal.current_directory) zipf.write(full_path, arcname) return CommandResult( success=True, output=f"Created archive: {archive_name}", exit_code=0, command=f"zip {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"zip {' '.join(args)}" ) def unzip(self, args: List[str]) -> CommandResult: """Extract ZIP archive""" if not args: return CommandResult( success=False, output="", error="Usage: unzip [destination]", exit_code=1, command="unzip" ) archive_name = args[0] archive_path = os.path.join(self.terminal.current_directory, archive_name) destination = args[1] if len(args) > 1 else os.path.splitext(archive_name)[0] dest_path = os.path.join(self.terminal.current_directory, destination) try: with zipfile.ZipFile(archive_path, 'r') as zipf: zipf.extractall(dest_path) return CommandResult( success=True, output=f"Extracted {archive_name} to {destination}", exit_code=0, command=f"unzip {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"unzip {' '.join(args)}" ) def download(self, args: List[str]) -> CommandResult: """Download file from URL""" if not args: return CommandResult( success=False, output="", error="Usage: download [output_filename]", exit_code=1, command="download" ) url = args[0] output_filename = args[1] if len(args) > 1 else os.path.basename(url) output_path = os.path.join(self.terminal.current_directory, output_filename) try: urllib.request.urlretrieve(url, output_path) return CommandResult( success=True, output=f"Downloaded {url} to {output_filename}", exit_code=0, command=f"download {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"download {' '.join(args)}" ) class ProcessManager: """Manages system processes""" def __init__(self, terminal): self.terminal = terminal self.background_processes: Dict[int, subprocess.Popen] = {} def ps(self, args: List[str]) -> CommandResult: """List running processes""" output = [] # Header output.append(f"{'PID':<8} {'PPID':<8} {'CPU%':<8} {'MEM%':<8} {'RSS(MB)':<10} {'STATUS':<12} {'NAME':<20}") output.append("-" * 80) for proc in psutil.process_iter(['pid', 'ppid', 'name', 'cpu_percent', 'memory_percent', 'memory_info', 'status']): try: info = proc.info mem_mb = info['memory_info'].rss / 1024 / 1024 if info['memory_info'] else 0 output.append( f"{info['pid']:<8} " f"{info['ppid']:<8} " f"{info['cpu_percent']:<8.1f} " f"{info['memory_percent']:<8.1f} " f"{mem_mb:<10.1f} " f"{info['status']:<12} " f"{info['name'][:20]:<20}" ) except (psutil.NoSuchProcess, psutil.AccessDenied): continue return CommandResult( success=True, output='\n'.join(output), exit_code=0, command="ps" ) def kill(self, args: List[str]) -> CommandResult: """Terminate processes""" if not args: return CommandResult( success=False, output="", error="Usage: kill [-9] ", exit_code=1, command="kill" ) force = '-9' in args args = [arg for arg in args if arg != '-9'] output = [] errors = [] for pid_str in args: try: pid = int(pid_str) if force: proc = psutil.Process(pid) proc.kill() else: proc = psutil.Process(pid) proc.terminate() output.append(f"Terminated process {pid}") except ValueError: errors.append(f"Invalid PID: {pid_str}") except psutil.NoSuchProcess: errors.append(f"Process {pid_str} not found") except Exception as e: errors.append(f"Failed to kill {pid_str}: {str(e)}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"kill {' '.join(args)}" ) def killall(self, args: List[str]) -> CommandResult: """Kill processes by name""" if not args: return CommandResult( success=False, output="", error="Usage: killall ", exit_code=1, command="killall" ) process_name = args[0] killed = [] for proc in psutil.process_iter(['pid', 'name']): try: if proc.info['name'] and process_name.lower() in proc.info['name'].lower(): proc.terminate() killed.append(str(proc.info['pid'])) except (psutil.NoSuchProcess, psutil.AccessDenied): continue if killed: return CommandResult( success=True, output=f"Killed {len(killed)} processes matching '{process_name}'\nPIDs: {', '.join(killed)}", exit_code=0, command=f"killall {' '.join(args)}" ) else: return CommandResult( success=False, output="", error=f"No processes found matching '{process_name}'", exit_code=1, command=f"killall {' '.join(args)}" ) def top(self, args: List[str]) -> CommandResult: """Display top processes""" processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']): try: info = proc.info processes.append({ 'pid': info['pid'], 'name': info['name'][:30], 'cpu': info['cpu_percent'], 'mem': info['memory_percent'] }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue processes.sort(key=lambda x: x['cpu'], reverse=True) processes = processes[:20] output = [f"{'PID':<8} {'CPU%':<8} {'MEM%':<8} {'NAME':<30}"] output.append("-" * 55) for proc in processes: output.append(f"{proc['pid']:<8} {proc['cpu']:<8.1f} {proc['mem']:<8.1f} {proc['name']:<30}") # System info cpu_percent = psutil.cpu_percent(interval=0.5) memory = psutil.virtual_memory() output.append("") output.append(f"CPU Usage: {cpu_percent}%") output.append(f"Memory Usage: {memory.percent}% (Used: {memory.used // (1024**3)}GB / Total: {memory.total // (1024**3)}GB)") return CommandResult( success=True, output='\n'.join(output), exit_code=0, command="top" ) class NetworkManager: """Manages network operations""" def __init__(self, terminal): self.terminal = terminal def ping(self, args: List[str]) -> CommandResult: """Ping a host""" if not args: return CommandResult( success=False, output="", error="Usage: ping [count]", exit_code=1, command="ping" ) host = args[0] count = int(args[1]) if len(args) > 1 else 4 param = '-n' if IS_WINDOWS else '-c' try: result = subprocess.run( ['ping', param, str(count), host], capture_output=True, text=True, timeout=30 ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command=f"ping {' '.join(args)}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Ping timed out", exit_code=1, command=f"ping {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"ping {' '.join(args)}" ) def netstat(self, args: List[str]) -> CommandResult: """Display network connections""" output = [] for conn in psutil.net_connections(kind='inet'): try: laddr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "-" raddr = f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else "-" status = conn.status pid = conn.pid if conn.pid else "-" output.append(f"{pid:<8} {laddr:<25} {raddr:<25} {status:<15}") except: continue if output: header = f"{'PID':<8} {'LOCAL ADDRESS':<25} {'REMOTE ADDRESS':<25} {'STATUS':<15}" output.insert(0, header) output.insert(1, "-" * 75) return CommandResult( success=True, output='\n'.join(output) if output else "No network connections found", exit_code=0, command="netstat" ) def ifconfig(self, args: List[str]) -> CommandResult: """Display network interfaces""" output = [] for interface, addrs in psutil.net_if_addrs().items(): output.append(f"\n{interface}:") for addr in addrs: if addr.family == socket.AF_INET: output.append(f" IPv4: {addr.address}") if addr.broadcast: output.append(f" Broadcast: {addr.broadcast}") elif addr.family == socket.AF_INET6: output.append(f" IPv6: {addr.address}") elif hasattr(socket, 'AF_PACKET') and addr.family == socket.AF_PACKET: output.append(f" MAC: {addr.address}") stats = psutil.net_if_stats() for interface, stat in stats.items(): if stat.isup: output.append(f" Status: UP (Speed: {stat.speed} Mbps)") else: output.append(f" Status: DOWN") return CommandResult( success=True, output='\n'.join(output), exit_code=0, command="ifconfig" ) def wget(self, args: List[str]) -> CommandResult: """Download file from URL (alias for download)""" return self.terminal.fs_manager.download(args) def curl(self, args: List[str]) -> CommandResult: """Fetch URL content""" if not args: return CommandResult( success=False, output="", error="Usage: curl ", exit_code=1, command="curl" ) url = args[0] try: with urllib.request.urlopen(url, timeout=30) as response: content = response.read().decode('utf-8', errors='ignore') return CommandResult( success=True, output=content[:10000] + ('...' if len(content) > 10000 else ''), exit_code=0, command=f"curl {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"curl {' '.join(args)}" ) class PythonManager: """Manages Python-related operations""" def __init__(self, terminal): self.terminal = terminal def run_python(self, args: List[str]) -> CommandResult: """Run Python code or script""" if not args: # Start Python REPL return self._start_python_repl() script = args[0] try: if os.path.exists(os.path.join(self.terminal.current_directory, script)): # Run script file python_cmd = self.terminal.venv_manager.venv_python_path if self.terminal.venv_manager.current_venv else sys.executable result = subprocess.run( [python_cmd, script] + args[1:], capture_output=True, text=True, timeout=60, cwd=self.terminal.current_directory ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command=f"python {' '.join(args)}" ) else: # Run code string python_cmd = self.terminal.venv_manager.venv_python_path if self.terminal.venv_manager.current_venv else sys.executable result = subprocess.run( [python_cmd, '-c', script], capture_output=True, text=True, timeout=30 ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command=f"python {' '.join(args)}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Execution timed out", exit_code=1, command=f"python {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"python {' '.join(args)}" ) def _start_python_repl(self) -> CommandResult: """Start Python interactive REPL""" try: python_cmd = self.terminal.venv_manager.venv_python_path if self.terminal.venv_manager.current_venv else sys.executable subprocess.run([python_cmd], cwd=self.terminal.current_directory) return CommandResult( success=True, output="Python REPL exited", exit_code=0, command="python" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command="python" ) def pip(self, args: List[str]) -> CommandResult: """Run pip command""" if not args: return CommandResult( success=False, output="", error="Usage: pip [options]", exit_code=1, command="pip" ) if self.terminal.venv_manager.current_venv: pip_cmd = self.terminal.venv_manager.venv_pip_path else: pip_cmd = shutil.which('pip') or shutil.which('pip3') if not pip_cmd: return CommandResult( success=False, output="", error="pip not found. Please install pip or activate a virtual environment.", exit_code=1, command="pip" ) try: result = subprocess.run( [pip_cmd] + args, capture_output=True, text=True, timeout=300, cwd=self.terminal.current_directory ) return CommandResult( success=result.returncode == 0, output=result.stdout, error=result.stderr if result.returncode != 0 else "", exit_code=result.returncode, command=f"pip {' '.join(args)}" ) except subprocess.TimeoutExpired: return CommandResult( success=False, output="", error="Command timed out", exit_code=1, command=f"pip {' '.join(args)}" ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=f"pip {' '.join(args)}" ) class SystemManager: """Manages system operations""" def __init__(self, terminal): self.terminal = terminal def whoami(self, args: List[str]) -> CommandResult: """Display current user""" return CommandResult( success=True, output=os.getlogin(), exit_code=0, command="whoami" ) def hostname(self, args: List[str]) -> CommandResult: """Display hostname""" return CommandResult( success=True, output=platform.node(), exit_code=0, command="hostname" ) def uname(self, args: List[str]) -> CommandResult: """Display system information""" output = [] if '-a' in args or len(args) == 0: output.append(f"System: {platform.system()}") output.append(f"Node: {platform.node()}") output.append(f"Release: {platform.release()}") output.append(f"Version: {platform.version()}") output.append(f"Machine: {platform.machine()}") output.append(f"Processor: {platform.processor()}") else: if '-s' in args: output.append(platform.system()) if '-n' in args: output.append(platform.node()) if '-r' in args: output.append(platform.release()) if '-v' in args: output.append(platform.version()) if '-m' in args: output.append(platform.machine()) return CommandResult( success=True, output='\n'.join(output), exit_code=0, command=f"uname {' '.join(args)}" ) def env(self, args: List[str]) -> CommandResult: """Display environment variables""" env_vars = self.terminal.env_manager.get_all() output = '\n'.join([f"{k}={v}" for k, v in sorted(env_vars.items())]) return CommandResult( success=True, output=output, exit_code=0, command="env" ) def set_env(self, args: List[str]) -> CommandResult: """Set environment variable""" if not args: return CommandResult( success=False, output="", error="Usage: export VAR=value", exit_code=1, command="export" ) output = [] errors = [] for arg in args: if '=' in arg: key, value = arg.split('=', 1) self.terminal.env_manager.set(key, value, permanent=True) output.append(f"Set {key}={value}") else: errors.append(f"Ignoring invalid format: {arg}") return CommandResult( success=len(errors) == 0, output='\n'.join(output), error='\n'.join(errors) if errors else "", exit_code=0 if not errors else 1, command=f"export {' '.join(args)}" ) def unset_env(self, args: List[str]) -> CommandResult: """Unset environment variable""" if not args: return CommandResult( success=False, output="", error="Usage: unset VAR", exit_code=1, command="unset" ) output = [] for var in args: self.terminal.env_manager.unset(var) output.append(f"Unset {var}") return CommandResult( success=True, output='\n'.join(output), exit_code=0, command=f"unset {' '.join(args)}" ) def echo(self, args: List[str]) -> CommandResult: """Echo arguments""" output = ' '.join(args) output = self.terminal.env_manager.expand(output) return CommandResult( success=True, output=output, exit_code=0, command=f"echo {' '.join(args)}" ) def clear(self, args: List[str]) -> CommandResult: """Clear screen""" os.system('cls' if IS_WINDOWS else 'clear') return CommandResult( success=True, output="", exit_code=0, command="clear" ) def exit(self, args: List[str]) -> CommandResult: """Exit terminal""" self.terminal.running = False return CommandResult( success=True, output="Goodbye!", exit_code=0, command="exit" ) def help(self, args: List[str]) -> CommandResult: """Display help""" help_text = self.terminal.get_help_text() return CommandResult( success=True, output=help_text, exit_code=0, command="help" ) def history(self, args: List[str]) -> CommandResult: """Display command history""" history = self.terminal.history_manager.history[-50:] if args else self.terminal.history_manager.history output = [] for i, cmd in enumerate(history, 1): output.append(f"{i:4} {cmd}") if not output: output = ["No command history"] return CommandResult( success=True, output='\n'.join(output), exit_code=0, command="history" ) def date(self, args: List[str]) -> CommandResult: """Display current date and time""" now = datetime.now() return CommandResult( success=True, output=now.strftime("%a %b %d %H:%M:%S %Z %Y"), exit_code=0, command="date" ) def cal(self, args: List[str]) -> CommandResult: """Display calendar""" now = datetime.now() import calendar year = int(args[0]) if args else now.year month = int(args[1]) if len(args) > 1 else now.month cal = calendar.month(year, month) return CommandResult( success=True, output=cal, exit_code=0, command=f"cal {' '.join(args)}" ) def sleep(self, args: List[str]) -> CommandResult: """Sleep for specified seconds""" if not args: return CommandResult( success=False, output="", error="Usage: sleep ", exit_code=1, command="sleep" ) try: seconds = float(args[0]) time.sleep(seconds) return CommandResult( success=True, output=f"Slept for {seconds} seconds", exit_code=0, command=f"sleep {' '.join(args)}" ) except ValueError: return CommandResult( success=False, output="", error="Invalid number", exit_code=1, command=f"sleep {' '.join(args)}" ) def which(self, args: List[str]) -> CommandResult: """Locate a command""" if not args: return CommandResult( success=False, output="", error="Usage: which ", exit_code=1, command="which" ) command = args[0] path = shutil.which(command) if path: return CommandResult( success=True, output=path, exit_code=0, command=f"which {' '.join(args)}" ) else: return CommandResult( success=False, output="", error=f"{command} not found", exit_code=1, command=f"which {' '.join(args)}" ) def alias(self, args: List[str]) -> CommandResult: """Manage aliases""" if not args: # List all aliases aliases = self.terminal.alias_manager.list_aliases() if aliases: output = "Aliases:\n" + '\n'.join([f" alias {k}='{v}'" for k, v in aliases.items()]) else: output = "No aliases defined" return CommandResult( success=True, output=output, exit_code=0, command="alias" ) # Parse alias name='command' full_command = ' '.join(args) if '=' not in full_command: return CommandResult( success=False, output="", error="Usage: alias name='command'", exit_code=1, command="alias" ) name, command = full_command.split('=', 1) name = name.strip() command = command.strip("'\"") if self.terminal.alias_manager.add_alias(name, command): return CommandResult( success=True, output=f"Alias created: {name} -> {command}", exit_code=0, command=f"alias {name}='{command}'" ) else: return CommandResult( success=False, output="", error="Invalid alias name", exit_code=1, command="alias" ) def unalias(self, args: List[str]) -> CommandResult: """Remove an alias""" if not args: return CommandResult( success=False, output="", error="Usage: unalias ", exit_code=1, command="unalias" ) name = args[0] if self.terminal.alias_manager.remove_alias(name): return CommandResult( success=True, output=f"Alias removed: {name}", exit_code=0, command=f"unalias {name}" ) else: return CommandResult( success=False, output="", error=f"Alias '{name}' not found", exit_code=1, command=f"unalias {name}" ) class TerminalEmulator: """Full-featured Terminal Emulator""" def __init__(self, output_callback: Callable = None, input_callback: Callable = None, user_id: str = None): """ Initialize terminal emulator Args: output_callback: Function to call for output (for web integration) input_callback: Function to call for input (for web integration) user_id: User ID for isolated workspace (for multi-user support) """ # Set workspace as root - this is the ONLY directory the terminal can see # Use user-specific workspace if user_id provided script_dir = os.path.dirname(os.path.abspath(__file__)) if user_id: # User-specific workspace under /data/users/{user_id}/workspace workspace_root = os.path.join('/data/users', str(user_id), 'workspace') os.makedirs(workspace_root, exist_ok=True) else: # Fallback to script directory for backward compatibility workspace_root = script_dir # If there's a workspace.json in parent, use that directory if os.path.exists(os.path.join(script_dir, 'workspace.json')): workspace_root = script_dir elif os.path.exists(os.path.join(script_dir, '..', 'workspace.json')): workspace_root = os.path.dirname(script_dir) self.user_id = user_id self.workspace_root = workspace_root self.current_directory = workspace_root os.chdir(workspace_root) self.running = True self.output_callback = output_callback self.input_callback = input_callback # Initialize managers self.env_manager = EnvironmentManager() self.history_manager = HistoryManager() self.alias_manager = AliasManager() self.venv_manager = VirtualEnvironmentManager(self) self.fs_manager = FileSystemManager(self) self.process_manager = ProcessManager(self) self.network_manager = NetworkManager(self) self.python_manager = PythonManager(self) self.system_manager = SystemManager(self) # Command handlers mapping self.command_handlers: Dict[str, Callable] = { # Navigation 'cd': self.fs_manager.cd, 'pwd': self.fs_manager.pwd, 'ls': self.fs_manager.ls, 'dir': self.fs_manager.ls, 'tree': self.fs_manager.tree, # File operations 'mkdir': self.fs_manager.mkdir, 'rmdir': self.fs_manager.rmdir, 'rm': self.fs_manager.rm, 'cp': self.fs_manager.cp, 'copy': self.fs_manager.cp, 'mv': self.fs_manager.mv, 'move': self.fs_manager.mv, 'touch': self.fs_manager.touch, 'cat': self.fs_manager.cat, 'type': self.fs_manager.cat, 'head': self.fs_manager.head, 'tail': self.fs_manager.tail, 'find': self.fs_manager.find, 'grep': self.fs_manager.grep, 'du': self.fs_manager.du, # Archive operations 'zip': self.fs_manager.zip_files, 'unzip': self.fs_manager.unzip, # Download operations 'download': self.fs_manager.download, 'wget': self.network_manager.wget, 'curl': self.network_manager.curl, # Process management 'ps': self.process_manager.ps, 'kill': self.process_manager.kill, 'killall': self.process_manager.killall, 'top': self.process_manager.top, # Network 'ping': self.network_manager.ping, 'netstat': self.network_manager.netstat, 'ifconfig': self.network_manager.ifconfig, 'ipconfig': self.network_manager.ifconfig, # Python 'python': self.python_manager.run_python, 'python3': self.python_manager.run_python, 'pip': self.python_manager.pip, 'pip3': self.python_manager.pip, # Virtual Environment 'venv': self.venv_command, 'deactivate': lambda args: self.venv_manager.deactivate_venv(), # System 'whoami': self.system_manager.whoami, 'hostname': self.system_manager.hostname, 'uname': self.system_manager.uname, 'env': self.system_manager.env, 'export': self.system_manager.set_env, 'set': self.system_manager.set_env, 'unset': self.system_manager.unset_env, 'echo': self.system_manager.echo, 'clear': self.system_manager.clear, 'cls': self.system_manager.clear, 'exit': self.system_manager.exit, 'quit': self.system_manager.exit, 'help': self.system_manager.help, 'history': self.system_manager.history, 'date': self.system_manager.date, 'cal': self.system_manager.cal, 'sleep': self.system_manager.sleep, 'which': self.system_manager.which, 'alias': self.system_manager.alias, 'unalias': self.system_manager.unalias, } def venv_command(self, args: List[str]) -> CommandResult: """Virtual environment management command""" if not args: output = "Virtual Environment Commands:\n" output += " venv create - Create new virtual environment\n" output += " venv create -p - Create venv at specific path\n" output += " venv activate - Activate virtual environment\n" output += " venv deactivate - Deactivate current venv\n" output += " venv list - List all venvs in current directory\n" output += " venv install - Install package in active venv\n" output += " venv uninstall - Uninstall package from active venv\n" output += " venv list-packages - List installed packages\n" output += " venv freeze [file] - Generate requirements.txt\n" output += " venv install-reqs [file] - Install from requirements.txt\n" output += " venv info - Show active venv information" return CommandResult( success=True, output=output, exit_code=0, command="venv" ) subcommand = args[0] if subcommand == "create": if len(args) < 2: return CommandResult( success=False, output="", error="Usage: venv create [-p ]", exit_code=1, command="venv create" ) name = args[1] path = None if '-p' in args: p_idx = args.index('-p') if p_idx + 1 < len(args): path = args[p_idx + 1] return self.venv_manager.create_venv(name, path) elif subcommand == "activate": if len(args) < 2: return CommandResult( success=False, output="", error="Usage: venv activate ", exit_code=1, command="venv activate" ) return self.venv_manager.activate_venv(args[1]) elif subcommand == "deactivate": return self.venv_manager.deactivate_venv() elif subcommand == "list": return self.venv_manager.list_venvs() elif subcommand == "install": if len(args) < 2: return CommandResult( success=False, output="", error="Usage: venv install [version]", exit_code=1, command="venv install" ) package = args[1] version = args[2] if len(args) > 2 else None return self.venv_manager.install_package(package, version) elif subcommand == "uninstall": if len(args) < 2: return CommandResult( success=False, output="", error="Usage: venv uninstall ", exit_code=1, command="venv uninstall" ) return self.venv_manager.uninstall_package(args[1]) elif subcommand == "list-packages": return self.venv_manager.list_packages() elif subcommand == "freeze": filename = args[1] if len(args) > 1 else "requirements.txt" return self.venv_manager.freeze_requirements(filename) elif subcommand == "install-reqs": filename = args[1] if len(args) > 1 else "requirements.txt" return self.venv_manager.install_requirements(filename) elif subcommand == "info": info = self.venv_manager.get_venv_info() if info["active"]: output = f"Active Virtual Environment:\n" output += f" Name: {info['name']}\n" output += f" Path: {info['path']}\n" output += f" Python: {info['python']}\n" output += f" Pip: {info['pip']}" else: output = "No virtual environment is currently active" return CommandResult( success=True, output=output, exit_code=0, command="venv info" ) else: return CommandResult( success=False, output="", error=f"Unknown venv subcommand: {subcommand}", exit_code=1, command=f"venv {subcommand}" ) def get_prompt(self) -> str: """Get the terminal prompt string - PowerShell style with friendly path""" venv_indicator = "" if self.venv_manager.current_venv: venv_indicator = f"({self.venv_manager.current_venv}) " # Get friendly path using the fs_manager's method friendly_path = self.fs_manager._get_friendly_path(self.current_directory) # PowerShell style prompt: PS HenAi:\Workspace\subfolder> return f"{Colors.GREEN}{venv_indicator}PS{Colors.RESET} {Colors.CYAN}{friendly_path}{Colors.RESET}> " def get_plain_prompt(self) -> str: """Get plain text prompt (for web integration)""" venv_indicator = "" if self.venv_manager.current_venv: venv_indicator = f"({self.venv_manager.current_venv}) " friendly_path = self.fs_manager._get_friendly_path(self.current_directory) return f"{venv_indicator}PS {friendly_path}> " def execute_command(self, command_line: str) -> CommandResult: """ Execute a command and return the result Args: command_line: The command line to execute Returns: CommandResult object with output and status """ if not command_line or not command_line.strip(): return CommandResult(success=True, output="", exit_code=0, command="") start_time = time.time() # Add to history self.history_manager.add_command(command_line) # Expand aliases command_line = self.alias_manager.expand(command_line) # Parse command parts = self._parse_command_line(command_line) if not parts: return CommandResult(success=True, output="", exit_code=0, command=command_line) command = parts[0].lower() args = parts[1:] # Handle pipes if '|' in command_line: return self._handle_pipes(command_line) # Handle output redirection if '>' in command_line: return self._handle_redirection(command_line, '>') if '>>' in command_line: return self._handle_redirection(command_line, '>>') # Handle background execution background = False if args and args[-1] == '&': background = True args = args[:-1] # Execute built-in command if command in self.command_handlers: handler = self.command_handlers[command] result = handler(args) # If this was a cd command, update the current directory in the result if command == 'cd' and result.success and result.output: # The output contains the new friendly path pass else: # Execute as system command result = self._execute_system_command(command_line) result.execution_time = time.time() - start_time result.command = command_line # Handle background execution if background and result.success: result.output = f"[Background] {result.output}" return result def _parse_command_line(self, command_line: str) -> List[str]: """Parse command line respecting quotes""" import shlex try: return shlex.split(command_line) except ValueError: # Fallback to simple split return command_line.split() def _execute_system_command(self, command: str) -> CommandResult: """Execute a system command using subprocess""" try: shell = IS_WINDOWS process = subprocess.Popen( command, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.current_directory, env=self.env_manager.get_all() ) stdout, stderr = process.communicate(timeout=300) return CommandResult( success=process.returncode == 0, output=stdout, error=stderr if process.returncode != 0 else "", exit_code=process.returncode, command=command ) except subprocess.TimeoutExpired: process.kill() return CommandResult( success=False, output="", error="Command timed out after 300 seconds", exit_code=124, command=command ) except FileNotFoundError: return CommandResult( success=False, output="", error=f"Command not found: {command.split()[0] if command else command}", exit_code=127, command=command ) except Exception as e: return CommandResult( success=False, output="", error=str(e), exit_code=1, command=command ) def _handle_pipes(self, command_line: str) -> CommandResult: """Handle pipe operations between commands""" commands = [cmd.strip() for cmd in command_line.split('|')] processes = [] prev_output = None for i, cmd in enumerate(commands): parts = self._parse_command_line(cmd) if not parts: continue if parts[0] in self.command_handlers: # Built-in command - can't pipe easily, execute directly handler = self.command_handlers[parts[0]] result = handler(parts[1:]) if prev_output: result.output = prev_output + result.output prev_output = result.output else: # External command try: if prev_output: # Pass previous output as input process = subprocess.Popen( cmd, shell=IS_WINDOWS, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.current_directory ) stdout, stderr = process.communicate(input=prev_output, timeout=60) else: process = subprocess.Popen( cmd, shell=IS_WINDOWS, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.current_directory ) stdout, stderr = process.communicate(timeout=60) prev_output = stdout if stderr: prev_output += stderr except Exception as e: return CommandResult( success=False, output="", error=f"Pipe error: {str(e)}", exit_code=1, command=command_line ) return CommandResult( success=True, output=prev_output or "", exit_code=0, command=command_line ) def _handle_redirection(self, command_line: str, redir_op: str) -> CommandResult: """Handle output redirection to files""" parts = command_line.split(redir_op, 1) if len(parts) != 2: return self._execute_system_command(command_line) command = parts[0].strip() filename = parts[1].strip() result = self.execute_command(command) if result.success or result.output: mode = 'a' if redir_op == '>>' else 'w' try: filepath = os.path.join(self.current_directory, filename) with open(filepath, mode, encoding='utf-8') as f: f.write(result.output) result.output = f"Output written to {filename}" except Exception as e: result.error = f"Failed to write to {filename}: {str(e)}" result.success = False return result def get_help_text(self) -> str: """Get help text for all commands""" return f""" {Colors.BOLD}{Colors.CYAN}═══════════════════════════════════════════════════════════════════════════════{Colors.RESET} {Colors.BOLD}HenAi Terminal Emulator v2.0 - Complete Command Reference{Colors.RESET} {Colors.BOLD}{Colors.CYAN}═══════════════════════════════════════════════════════════════════════════════{Colors.RESET} {Colors.BOLD}{Colors.GREEN}📁 NAVIGATION & FILE OPERATIONS{Colors.RESET} {Colors.YELLOW}cd [dir]{Colors.RESET} - Change directory {Colors.YELLOW}pwd{Colors.RESET} - Print working directory {Colors.YELLOW}ls [-a] [-l] [-h] [-R]{Colors.RESET} - List directory contents {Colors.YELLOW}tree [-a] [-L N]{Colors.RESET} - Display directory tree {Colors.YELLOW}mkdir [-p] {Colors.RESET} - Create directory {Colors.YELLOW}rmdir {Colors.RESET} - Remove empty directory {Colors.YELLOW}rm [-r] [-f] {Colors.RESET} - Remove files/directories {Colors.YELLOW}cp [-r] {Colors.RESET} - Copy files/directories {Colors.YELLOW}mv {Colors.RESET} - Move/rename files {Colors.YELLOW}touch {Colors.RESET} - Create file or update timestamp {Colors.YELLOW}cat {Colors.RESET} - Display file contents {Colors.YELLOW}head [-n N] {Colors.RESET} - Display first N lines {Colors.YELLOW}tail [-n N] {Colors.RESET} - Display last N lines {Colors.YELLOW}find -name {Colors.RESET} - Search for files {Colors.YELLOW}grep [-i] {Colors.RESET} - Search in files {Colors.YELLOW}du [-h] [-s]{Colors.RESET} - Disk usage {Colors.BOLD}{Colors.GREEN}🐍 PYTHON & VIRTUAL ENVIRONMENT{Colors.RESET} {Colors.YELLOW}python [script]{Colors.RESET} - Run Python code/REPL {Colors.YELLOW}pip {Colors.RESET} - Python package manager {Colors.YELLOW}venv create {Colors.RESET} - Create virtual environment {Colors.YELLOW}venv activate {Colors.RESET} - Activate venv {Colors.YELLOW}venv deactivate{Colors.RESET} - Deactivate venv {Colors.YELLOW}venv list{Colors.RESET} - List virtual environments {Colors.YELLOW}venv install {Colors.RESET} - Install package in venv {Colors.YELLOW}venv freeze [file]{Colors.RESET} - Generate requirements.txt {Colors.BOLD}{Colors.GREEN}📦 ARCHIVE & DOWNLOAD{Colors.RESET} {Colors.YELLOW}zip {Colors.RESET} - Create ZIP archive {Colors.YELLOW}unzip [dest]{Colors.RESET} - Extract ZIP {Colors.YELLOW}download [filename]{Colors.RESET} - Download file {Colors.YELLOW}wget {Colors.RESET} - Download file (alias) {Colors.YELLOW}curl {Colors.RESET} - Fetch URL content {Colors.BOLD}{Colors.GREEN}🔧 PROCESS MANAGEMENT{Colors.RESET} {Colors.YELLOW}ps{Colors.RESET} - List processes {Colors.YELLOW}top{Colors.RESET} - Display top processes {Colors.YELLOW}kill [-9] {Colors.RESET} - Terminate process {Colors.YELLOW}killall {Colors.RESET} - Kill processes by name {Colors.BOLD}{Colors.GREEN}🌐 NETWORK{Colors.RESET} {Colors.YELLOW}ping [count]{Colors.RESET} - Ping a host {Colors.YELLOW}netstat{Colors.RESET} - Network connections {Colors.YELLOW}ifconfig{Colors.RESET} - Network interfaces {Colors.BOLD}{Colors.GREEN}⚙️ SYSTEM{Colors.RESET} {Colors.YELLOW}whoami{Colors.RESET} - Current user {Colors.YELLOW}hostname{Colors.RESET} - System hostname {Colors.YELLOW}uname [-a]{Colors.RESET} - System information {Colors.YELLOW}env{Colors.RESET} - Environment variables {Colors.YELLOW}export VAR=value{Colors.RESET} - Set environment variable {Colors.YELLOW}echo {Colors.RESET} - Display text {Colors.YELLOW}clear{Colors.RESET} - Clear screen {Colors.YELLOW}date{Colors.RESET} - Current date/time {Colors.YELLOW}cal [month] [year]{Colors.RESET} - Calendar {Colors.YELLOW}sleep {Colors.RESET} - Delay execution {Colors.YELLOW}which {Colors.RESET} - Locate command {Colors.YELLOW}alias name='command'{Colors.RESET} - Create alias {Colors.YELLOW}unalias {Colors.RESET} - Remove alias {Colors.YELLOW}history{Colors.RESET} - Command history {Colors.YELLOW}help{Colors.RESET} - This help {Colors.YELLOW}exit{Colors.RESET} - Exit terminal {Colors.BOLD}{Colors.CYAN}═══════════════════════════════════════════════════════════════════════════════{Colors.RESET} {Colors.DIM}Tips: • Use ↑/↓ to navigate command history • Use Tab for command completion • Use | to pipe commands (e.g., ls | grep py) • Use > to redirect output to file (e.g., ls > files.txt) • Use & to run command in background • Type 'venv' for virtual environment help{Colors.RESET} """ def run_interactive(self): """Run terminal in interactive mode (for console)""" print(self.get_help_text()) while self.running: try: command = input(self.get_prompt()) result = self.execute_command(command) if result.output: print(result.output) if result.error: print(f"{Colors.RED}{result.error}{Colors.RESET}") except KeyboardInterrupt: print("^C") continue except EOFError: print() break except Exception as e: print(f"{Colors.RED}Error: {str(e)}{Colors.RESET}") print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}") def execute_and_get_output(self, command: str) -> Dict[str, Any]: """ Execute command and return output as dict (for web integration) Returns: Dict with keys: success, output, error, exit_code, execution_time """ result = self.execute_command(command) return { "success": result.success, "output": result.output, "error": result.error, "exit_code": result.exit_code, "execution_time": result.execution_time, "command": result.command } def create_terminal_blueprint(app): """Create Flask blueprint for terminal endpoints""" from flask import Blueprint, request, jsonify, session, g terminal_bp = Blueprint('terminal', __name__, url_prefix='/api/terminal') # Store terminal instances per user (user_id: {session_id: terminal}) terminals = {} def get_user_id_from_request(): """Extract user_id from request headers or Flask g object""" # First try from g (set by auth middleware) if hasattr(g, 'user_id') and g.user_id: return str(g.user_id) # Then try from headers (set by proxy) user_id = request.headers.get('X-User-ID') if user_id: return user_id # Fallback to session (for backward compatibility) return session.get('user_id') @terminal_bp.route('/execute', methods=['POST']) def execute(): """Execute a command in the terminal""" data = request.json command = data.get('command', '') session_id = data.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() # Create user-specific key for terminal storage terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: # Pass user_id to TerminalEmulator for isolated workspace terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] result = terminal.execute_and_get_output(command) return jsonify(result) @terminal_bp.route('/cwd', methods=['GET']) def get_cwd(): """Get current working directory""" session_id = request.args.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] return jsonify({"cwd": terminal.current_directory}) @terminal_bp.route('/cwd', methods=['POST']) def set_cwd(): """Set current working directory""" data = request.json path = data.get('path', '') session_id = data.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] # Ensure path is within user's workspace if path and os.path.exists(path) and os.path.isdir(path): # Validate path is within workspace_root try: if os.path.commonpath([path, terminal.workspace_root]) == terminal.workspace_root: terminal.current_directory = path os.chdir(path) return jsonify({"success": True, "cwd": path}) else: return jsonify({"success": False, "error": "Cannot navigate outside workspace"}), 403 except ValueError: return jsonify({"success": False, "error": "Invalid path"}), 400 else: return jsonify({"success": False, "error": "Directory not found"}), 404 @terminal_bp.route('/info', methods=['GET']) def get_info(): """Get terminal information""" session_id = request.args.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] return jsonify({ "cwd": terminal.current_directory, "workspace_root": terminal.workspace_root, "platform": platform.system(), "python_version": sys.version, "venv_active": terminal.venv_manager.current_venv is not None, "venv_name": terminal.venv_manager.current_venv, "user_id": user_id }) @terminal_bp.route('/reset', methods=['POST']) def reset_terminal(): """Reset terminal session""" data = request.json session_id = data.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id terminals[terminal_key] = TerminalEmulator(user_id=user_id) return jsonify({"success": True}) @terminal_bp.route('/venv/activate', methods=['POST']) def activate_venv(): """Activate a virtual environment""" data = request.json name = data.get('name', '') path = data.get('path', None) session_id = data.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] result = terminal.venv_manager.activate_venv(name, path) return jsonify({ "success": result.success, "output": result.output, "error": result.error }) @terminal_bp.route('/venv/deactivate', methods=['POST']) def deactivate_venv(): """Deactivate current virtual environment""" data = request.json session_id = data.get('session_id', 'default') # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] result = terminal.venv_manager.deactivate_venv() return jsonify({ "success": result.success, "output": result.output, "error": result.error }) @terminal_bp.route('/venv/list', methods=['GET']) def list_venvs(): """List virtual environments""" session_id = request.args.get('session_id', 'default') path = request.args.get('path', None) # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] result = terminal.venv_manager.list_venvs(path) return jsonify({ "success": result.success, "output": result.output, "error": result.error }) @terminal_bp.route('/files', methods=['GET']) def list_files(): """List files in current directory""" session_id = request.args.get('session_id', 'default') path = request.args.get('path', None) # Get user_id for isolation user_id = get_user_id_from_request() terminal_key = f"{user_id}:{session_id}" if user_id else session_id if terminal_key not in terminals: terminals[terminal_key] = TerminalEmulator(user_id=user_id) terminal = terminals[terminal_key] target_path = path if path else terminal.current_directory # Ensure path is within user's workspace try: if os.path.commonpath([target_path, terminal.workspace_root]) != terminal.workspace_root: return jsonify({ "success": False, "error": "Access denied - outside workspace" }), 403 except ValueError: return jsonify({ "success": False, "error": "Invalid path" }), 400 try: items = [] for item in os.listdir(target_path): item_path = os.path.join(target_path, item) items.append({ "name": item, "path": item_path, "is_dir": os.path.isdir(item_path), "size": os.path.getsize(item_path) if os.path.isfile(item_path) else 0, "modified": datetime.fromtimestamp(os.path.getmtime(item_path)).isoformat() }) items.sort(key=lambda x: (not x["is_dir"], x["name"].lower())) return jsonify({ "success": True, "cwd": target_path, "workspace_root": terminal.workspace_root, "files": items }) except Exception as e: return jsonify({ "success": False, "error": str(e) }), 500 return terminal_bp def main(): """Main entry point for standalone terminal""" terminal = TerminalEmulator() terminal.run_interactive() if __name__ == "__main__": main()