Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Utility functions for the Edison application.
|
||||
"""
|
||||
from . import markdown_utils
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Logging utilities for the Edison application.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
def setup_logging(verbose=False):
|
||||
"""
|
||||
Configure logging for the application.
|
||||
|
||||
Args:
|
||||
verbose: If True, enable DEBUG level messages. If False, show only WARNING and above.
|
||||
|
||||
Returns:
|
||||
logging.Logger: The configured logger.
|
||||
"""
|
||||
# Create logs directory if it doesn't exist
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
logs_dir = os.path.join(script_dir, "logs")
|
||||
os.makedirs(logs_dir, exist_ok=True)
|
||||
|
||||
# Determine log levels based on verbose flag
|
||||
# Always save INFO+ logs to file, but console output depends on verbose flag
|
||||
file_level = logging.INFO
|
||||
console_level = logging.DEBUG if verbose else logging.WARNING
|
||||
|
||||
# Configure file and console handlers with different levels
|
||||
file_handler = logging.FileHandler(os.path.join(logs_dir, 'edison.log'))
|
||||
file_handler.setLevel(file_level)
|
||||
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
console_handler.setLevel(console_level)
|
||||
|
||||
# We need to set the root logger level to the lowest of our handlers
|
||||
root_level = logging.DEBUG if verbose else logging.INFO
|
||||
|
||||
# Basic config with handlers
|
||||
logging.basicConfig(
|
||||
level=root_level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[file_handler, console_handler]
|
||||
)
|
||||
|
||||
# Always disable httpx and urllib3 debug logs unless in verbose mode
|
||||
if not verbose:
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
# Get the main application logger
|
||||
logger = logging.getLogger('edison')
|
||||
|
||||
# Log setup confirmation at appropriate level
|
||||
if verbose:
|
||||
logger.debug("Verbose logging enabled (DEBUG level)")
|
||||
else:
|
||||
logger.info("Standard logging configuration applied (INFO to file, WARNING to console)")
|
||||
|
||||
return logger
|
||||
|
||||
def get_logger(name):
|
||||
"""
|
||||
Get a logger for the specified name.
|
||||
|
||||
Args:
|
||||
name: The name of the logger.
|
||||
|
||||
Returns:
|
||||
logging.Logger: The logger.
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Markdown formatting utilities for terminal display.
|
||||
"""
|
||||
import re
|
||||
import io
|
||||
import sys
|
||||
from termcolor import colored
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.syntax import Syntax
|
||||
from rich.panel import Panel
|
||||
|
||||
def convert_markdown_to_terminal(text):
|
||||
"""
|
||||
Convert markdown formatting to terminal-friendly colored text.
|
||||
|
||||
Args:
|
||||
text (str): Markdown text to convert
|
||||
|
||||
Returns:
|
||||
str: Terminal-formatted text with ANSI color codes
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Handle code blocks with triple backticks
|
||||
text = re.sub(
|
||||
r'```(?:\w+)?\n(.*?)\n```',
|
||||
lambda m: '\n' + colored(m.group(1), 'white', attrs=['bold', 'dark']) + '\n',
|
||||
text,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# Handle inline code with single backticks
|
||||
text = re.sub(
|
||||
r'`([^`]+)`',
|
||||
lambda m: colored(m.group(1), 'white', attrs=['bold']),
|
||||
text
|
||||
)
|
||||
|
||||
# Handle bold text with ** or __
|
||||
text = re.sub(
|
||||
r'\*\*([^*]+)\*\*|__([^_]+)__',
|
||||
lambda m: colored(m.group(1) or m.group(2), attrs=['bold']),
|
||||
text
|
||||
)
|
||||
|
||||
# Handle italic text with * or _
|
||||
text = re.sub(
|
||||
r'\*([^*]+)\*|_([^_]+)_',
|
||||
lambda m: colored(m.group(1) or m.group(2), attrs=['underline']),
|
||||
text
|
||||
)
|
||||
|
||||
# Handle headers
|
||||
text = re.sub(
|
||||
r'^#{1,6}\s+(.+)$',
|
||||
lambda m: colored(m.group(1), 'cyan', attrs=['bold']),
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Handle unordered lists
|
||||
text = re.sub(
|
||||
r'^(\s*[-*•]\s+)(.+)$',
|
||||
lambda m: m.group(1) + colored(m.group(2), 'white'),
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Handle ordered lists
|
||||
text = re.sub(
|
||||
r'^(\s*\d+\.\s+)(.+)$',
|
||||
lambda m: m.group(1) + colored(m.group(2), 'white'),
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Handle links [text](url) - keep only the text part
|
||||
text = re.sub(
|
||||
r'\[([^\]]+)\]\([^)]+\)',
|
||||
lambda m: colored(m.group(1), 'blue', attrs=['underline']),
|
||||
text
|
||||
)
|
||||
|
||||
# Handle horizontal rules
|
||||
text = re.sub(
|
||||
r'^-{3,}$|^\*{3,}$|^_{3,}$',
|
||||
lambda m: colored('-' * 50, 'white', attrs=['dark']),
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Handle blockquotes
|
||||
text = re.sub(
|
||||
r'^>\s+(.+)$',
|
||||
lambda m: colored('│ ', 'cyan') + colored(m.group(1), 'cyan'),
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
def format_command_explanation(explanation, use_rich=False):
|
||||
"""
|
||||
Format a command explanation with appropriate terminal styling.
|
||||
|
||||
Args:
|
||||
explanation (str): The explanation text in markdown format
|
||||
use_rich (bool): Whether to use rich formatting
|
||||
|
||||
Returns:
|
||||
str: Formatted explanation with terminal-friendly styling
|
||||
"""
|
||||
if use_rich:
|
||||
return format_command_explanation_rich(explanation)
|
||||
|
||||
# First convert markdown to terminal format
|
||||
formatted_text = convert_markdown_to_terminal(explanation)
|
||||
|
||||
# Add additional formatting for common command explanation patterns
|
||||
|
||||
# Highlight command parts and common technical terms
|
||||
formatted_text = re.sub(
|
||||
r'\b(command|option|flag|argument|parameter|switch)\b',
|
||||
lambda m: colored(m.group(0), 'yellow'),
|
||||
formatted_text,
|
||||
flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
# Highlight file paths and patterns
|
||||
# More specific pattern to avoid matching things like "Unix/Linux"
|
||||
formatted_text = re.sub(
|
||||
r'(?:^|\s)(/(?:[\w.-]+/?)+)(?=\s|$|[,.;:])', # Path must start with / and have at least one directory component
|
||||
lambda m: colored(m.group(1), 'green'),
|
||||
formatted_text
|
||||
)
|
||||
|
||||
# Highlight command names and utilities
|
||||
# Common Unix/Linux commands
|
||||
formatted_text = re.sub(
|
||||
r'\b(ls|grep|find|awk|sed|cat|cp|mv|rm|mkdir|chmod|chown|ps|kill|top|df|du|tar|gzip|ssh|scp|curl|wget)\b',
|
||||
lambda m: colored(m.group(0), 'green'),
|
||||
formatted_text
|
||||
)
|
||||
|
||||
# Highlight "This command" starts of sentences
|
||||
formatted_text = re.sub(
|
||||
r'(^|[.!?]\s+)(This command|The command)',
|
||||
lambda m: m.group(1) + colored(m.group(2), 'cyan', attrs=['bold']),
|
||||
formatted_text
|
||||
)
|
||||
|
||||
return formatted_text
|
||||
|
||||
def format_command_explanation_rich(explanation):
|
||||
"""
|
||||
Format a command explanation with rich formatting.
|
||||
|
||||
Args:
|
||||
explanation (str): The explanation text in markdown format
|
||||
|
||||
Returns:
|
||||
str: Rich-formatted explanation as a string
|
||||
"""
|
||||
# Capture the rich output as a string
|
||||
str_io = io.StringIO()
|
||||
console = Console(file=str_io, width=80)
|
||||
|
||||
# Create a markdown object
|
||||
md = Markdown(explanation)
|
||||
|
||||
# Render in a panel with a title
|
||||
panel = Panel(md, title="Command Explanation", border_style="green")
|
||||
console.print(panel)
|
||||
|
||||
return str_io.getvalue()
|
||||
|
||||
def format_command_rich(command, shell="bash", theme="monokai"):
|
||||
"""
|
||||
Format a command with syntax highlighting using Rich.
|
||||
|
||||
Args:
|
||||
command (str): The command to format
|
||||
shell (str): The shell language for syntax highlighting
|
||||
theme (str): The color theme to use
|
||||
|
||||
Returns:
|
||||
str: Rich-formatted command as a string
|
||||
"""
|
||||
# Capture the rich output as a string
|
||||
str_io = io.StringIO()
|
||||
console = Console(file=str_io, width=80)
|
||||
|
||||
# Create a syntax object
|
||||
syntax = Syntax(command, shell, theme=theme, word_wrap=True)
|
||||
|
||||
# Render in a panel with a title
|
||||
panel = Panel(syntax, title="Command", border_style="blue")
|
||||
console.print(panel)
|
||||
|
||||
return str_io.getvalue()
|
||||
|
||||
def print_command_rich(command, shell="bash", theme="monokai"):
|
||||
"""
|
||||
Print a command with syntax highlighting using Rich.
|
||||
|
||||
Args:
|
||||
command (str): The command to print
|
||||
shell (str): The shell language for syntax highlighting
|
||||
theme (str): The color theme to use
|
||||
"""
|
||||
console = Console()
|
||||
syntax = Syntax(command, shell, theme=theme, word_wrap=True)
|
||||
panel = Panel(syntax, title="Command", border_style="blue")
|
||||
console.print(panel)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Operating system utilities for the Edison application.
|
||||
"""
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import distro
|
||||
|
||||
def get_os_friendly_name():
|
||||
"""
|
||||
Returns a friendly name of the user's operating system.
|
||||
|
||||
The function retrieves the current system platform name using the `platform.system()` function.
|
||||
For Linux, it appends the distribution name retrieved from `distro.name(pretty=True)` to give a
|
||||
more descriptive representation. For Darwin (Apple's macOS), it appends "macOS" to "Darwin" to
|
||||
make the output clearer to the user.
|
||||
|
||||
Returns:
|
||||
str: A friendly name for the user's operating system. It will be one of the following:
|
||||
- "Linux/<distribution name>"
|
||||
- "Darwin/macOS"
|
||||
- The system string returned by `platform.system()` if it's not Linux or Darwin.
|
||||
"""
|
||||
os_name = platform.system()
|
||||
|
||||
if os_name == "Linux":
|
||||
os_name = "Linux/" + distro.name(pretty=True)
|
||||
elif os_name == "Darwin":
|
||||
os_name = "Darwin/macOS"
|
||||
|
||||
return os_name
|
||||
|
||||
def missing_posix_display():
|
||||
"""
|
||||
Checks if the DISPLAY environment variable is set in a POSIX-compliant shell.
|
||||
|
||||
This function runs a shell subprocess that outputs the value of the DISPLAY environment
|
||||
variable. It then checks if this value is unset (i.e., equals a newline 'b'\\n'') in the
|
||||
current shell environment. If the DISPLAY variable is unset, the function returns `True`
|
||||
indicating a "missing" display; otherwise, it returns `False`.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the DISPLAY environment variable is unset or empty, `False` otherwise.
|
||||
"""
|
||||
if os.name != "posix":
|
||||
return False
|
||||
|
||||
display = subprocess.check_output("echo $DISPLAY", shell=True)
|
||||
return display == b'\n'
|
||||
|
||||
def get_default_shell():
|
||||
"""
|
||||
Get the default shell for the current operating system.
|
||||
|
||||
Returns:
|
||||
str: The default shell.
|
||||
"""
|
||||
# Unix based SHELL (/bin/bash, /bin/zsh), otherwise assuming it's Windows
|
||||
return os.environ.get("SHELL", "powershell.exe")
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Validation utilities for the Edison application.
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def is_dangerous_command(command):
|
||||
"""
|
||||
Check if a command contains potentially dangerous operations.
|
||||
|
||||
Args:
|
||||
command (str): The command to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the command is potentially dangerous, False otherwise.
|
||||
"""
|
||||
dangerous_patterns = [
|
||||
r"rm\s+-rf\s+/", # Remove recursively from root
|
||||
r"sudo\s+rm", # Sudo remove
|
||||
r"chmod\s+777", # Chmod 777 (too permissive)
|
||||
r">\s+/dev/", # Redirect to device
|
||||
r">\s+/etc/", # Redirect to system config
|
||||
r"mkfs", # Format filesystem
|
||||
r"dd\s+if=", # Disk destroyer
|
||||
r":(){:\|:};:", # Fork bomb
|
||||
]
|
||||
|
||||
for pattern in dangerous_patterns:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
logger.warning(f"Potentially dangerous command detected: {command}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_for_issue(response):
|
||||
"""
|
||||
Checks the given response for any issues.
|
||||
|
||||
Args:
|
||||
response (str): The response to check.
|
||||
|
||||
Returns:
|
||||
bool: True if there's an issue, False otherwise.
|
||||
"""
|
||||
prefixes = ("sorry", "i'm sorry", "the question is not clear", "i'm", "i am")
|
||||
return response.lower().startswith(prefixes)
|
||||
|
||||
def check_for_markdown(response):
|
||||
"""
|
||||
Checks for the presence of markdown formatting in the response.
|
||||
|
||||
Args:
|
||||
response (str): The response to check.
|
||||
|
||||
Returns:
|
||||
bool: True if markdown is detected, False otherwise.
|
||||
"""
|
||||
return response.count("```", 2)
|
||||
Reference in New Issue
Block a user