refactor: consolidate PM Agent optimization and pending changes

PM Agent optimization (already committed separately):
- superclaude/commands/pm.md: 1652→14 lines
- superclaude/agents/pm-agent.md: 735→429 lines
- docs/agents/pm-agent-guide.md: new guide file

Other pending changes:
- setup: framework_docs, mcp, logger, remove ui.py
- superclaude: __main__, cli/app, cli/commands/install
- tests: test_ui updates
- scripts: workflow metrics analysis tools
- docs/memory: session state updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kazuki
2025-10-17 04:54:31 +09:00
parent d168278879
commit a4ffe52724
13 changed files with 1298 additions and 1247 deletions

View File

@@ -1,7 +1,10 @@
"""Utility modules for SuperClaude installation system"""
"""Utility modules for SuperClaude installation system
Note: UI utilities (ProgressBar, Menu, confirm, Colors) have been removed.
The new CLI uses typer + rich natively via superclaude/cli/
"""
from .ui import ProgressBar, Menu, confirm, Colors
from .logger import Logger
from .security import SecurityValidator
__all__ = ["ProgressBar", "Menu", "confirm", "Colors", "Logger", "SecurityValidator"]
__all__ = ["Logger", "SecurityValidator"]

View File

@@ -9,10 +9,13 @@ from pathlib import Path
from typing import Optional, Dict, Any
from enum import Enum
from .ui import Colors
from rich.console import Console
from .symbols import symbols
from .paths import get_home_directory
# Rich console for colored output
console = Console()
class LogLevel(Enum):
"""Log levels"""
@@ -69,37 +72,23 @@ class Logger:
}
def _setup_console_handler(self) -> None:
"""Setup colorized console handler"""
handler = logging.StreamHandler(sys.stdout)
"""Setup colorized console handler using rich"""
from rich.logging import RichHandler
handler = RichHandler(
console=console,
show_time=False,
show_path=False,
markup=True,
rich_tracebacks=True,
tracebacks_show_locals=False,
)
handler.setLevel(self.console_level.value)
# Custom formatter with colors
class ColorFormatter(logging.Formatter):
def format(self, record):
# Color mapping
colors = {
"DEBUG": Colors.WHITE,
"INFO": Colors.BLUE,
"WARNING": Colors.YELLOW,
"ERROR": Colors.RED,
"CRITICAL": Colors.RED + Colors.BRIGHT,
}
# Simple formatter (rich handles coloring)
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
# Prefix mapping
prefixes = {
"DEBUG": "[DEBUG]",
"INFO": "[INFO]",
"WARNING": "[!]",
"ERROR": f"[{symbols.crossmark}]",
"CRITICAL": "[CRITICAL]",
}
color = colors.get(record.levelname, Colors.WHITE)
prefix = prefixes.get(record.levelname, "[LOG]")
return f"{color}{prefix} {record.getMessage()}{Colors.RESET}"
handler.setFormatter(ColorFormatter())
self.logger.addHandler(handler)
def _setup_file_handler(self) -> None:
@@ -130,7 +119,7 @@ class Logger:
except Exception as e:
# If file logging fails, continue with console only
print(f"{Colors.YELLOW}[!] Could not setup file logging: {e}{Colors.RESET}")
console.print(f"[yellow][!] Could not setup file logging: {e}[/yellow]")
self.log_file = None
def _cleanup_old_logs(self, keep_count: int = 10) -> None:
@@ -179,23 +168,9 @@ class Logger:
def success(self, message: str, **kwargs) -> None:
"""Log success message (info level with special formatting)"""
# Use a custom success formatter for console
if self.logger.handlers:
console_handler = self.logger.handlers[0]
if hasattr(console_handler, "formatter"):
original_format = console_handler.formatter.format
def success_format(record):
return f"{Colors.GREEN}[{symbols.checkmark}] {record.getMessage()}{Colors.RESET}"
console_handler.formatter.format = success_format
self.logger.info(message, **kwargs)
console_handler.formatter.format = original_format
else:
self.logger.info(f"SUCCESS: {message}", **kwargs)
else:
self.logger.info(f"SUCCESS: {message}", **kwargs)
# Use rich markup for success messages
success_msg = f"[green]{symbols.checkmark} {message}[/green]"
self.logger.info(success_msg, **kwargs)
self.log_counts["info"] += 1
def step(self, step: int, total: int, message: str, **kwargs) -> None:

View File

@@ -1,552 +0,0 @@
"""
User interface utilities for SuperClaude installation system
Cross-platform console UI with colors and progress indication
"""
import sys
import time
import shutil
import getpass
from typing import List, Optional, Any, Dict, Union
from enum import Enum
from .symbols import symbols, safe_print, format_with_symbols
# Try to import colorama for cross-platform color support
try:
import colorama
from colorama import Fore, Back, Style
colorama.init(autoreset=True)
COLORAMA_AVAILABLE = True
except ImportError:
COLORAMA_AVAILABLE = False
# Fallback color codes for Unix-like systems
class MockFore:
RED = "\033[91m" if sys.platform != "win32" else ""
GREEN = "\033[92m" if sys.platform != "win32" else ""
YELLOW = "\033[93m" if sys.platform != "win32" else ""
BLUE = "\033[94m" if sys.platform != "win32" else ""
MAGENTA = "\033[95m" if sys.platform != "win32" else ""
CYAN = "\033[96m" if sys.platform != "win32" else ""
WHITE = "\033[97m" if sys.platform != "win32" else ""
class MockStyle:
RESET_ALL = "\033[0m" if sys.platform != "win32" else ""
BRIGHT = "\033[1m" if sys.platform != "win32" else ""
Fore = MockFore()
Style = MockStyle()
class Colors:
"""Color constants for console output"""
RED = Fore.RED
GREEN = Fore.GREEN
YELLOW = Fore.YELLOW
BLUE = Fore.BLUE
MAGENTA = Fore.MAGENTA
CYAN = Fore.CYAN
WHITE = Fore.WHITE
RESET = Style.RESET_ALL
BRIGHT = Style.BRIGHT
class ProgressBar:
"""Cross-platform progress bar with customizable display"""
def __init__(self, total: int, width: int = 50, prefix: str = "", suffix: str = ""):
"""
Initialize progress bar
Args:
total: Total number of items to process
width: Width of progress bar in characters
prefix: Text to display before progress bar
suffix: Text to display after progress bar
"""
self.total = total
self.width = width
self.prefix = prefix
self.suffix = suffix
self.current = 0
self.start_time = time.time()
# Get terminal width for responsive display
try:
self.terminal_width = shutil.get_terminal_size().columns
except OSError:
self.terminal_width = 80
def update(self, current: int, message: str = "") -> None:
"""
Update progress bar
Args:
current: Current progress value
message: Optional message to display
"""
self.current = current
percent = min(100, (current / self.total) * 100) if self.total > 0 else 100
# Calculate filled and empty portions
filled_width = (
int(self.width * current / self.total) if self.total > 0 else self.width
)
filled = symbols.block_filled * filled_width
empty = symbols.block_empty * (self.width - filled_width)
# Calculate elapsed time and ETA
elapsed = time.time() - self.start_time
if current > 0:
eta = (elapsed / current) * (self.total - current)
eta_str = f" ETA: {self._format_time(eta)}"
else:
eta_str = ""
# Format progress line
if message:
status = f" {message}"
else:
status = ""
progress_line = (
f"\r{self.prefix}[{Colors.GREEN}{filled}{Colors.WHITE}{empty}{Colors.RESET}] "
f"{percent:5.1f}%{status}{eta_str}"
)
# Truncate if too long for terminal
max_length = self.terminal_width - 5
if len(progress_line) > max_length:
# Remove color codes for length calculation
plain_line = (
progress_line.replace(Colors.GREEN, "")
.replace(Colors.WHITE, "")
.replace(Colors.RESET, "")
)
if len(plain_line) > max_length:
progress_line = progress_line[:max_length] + "..."
safe_print(progress_line, end="", flush=True)
def increment(self, message: str = "") -> None:
"""
Increment progress by 1
Args:
message: Optional message to display
"""
self.update(self.current + 1, message)
def finish(self, message: str = "Complete") -> None:
"""
Complete progress bar
Args:
message: Completion message
"""
self.update(self.total, message)
print() # New line after completion
def _format_time(self, seconds: float) -> str:
"""Format time duration as human-readable string"""
if seconds < 60:
return f"{seconds:.0f}s"
elif seconds < 3600:
return f"{seconds/60:.0f}m {seconds%60:.0f}s"
else:
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return f"{hours:.0f}h {minutes:.0f}m"
class Menu:
"""Interactive menu system with keyboard navigation"""
def __init__(self, title: str, options: List[str], multi_select: bool = False):
"""
Initialize menu
Args:
title: Menu title
options: List of menu options
multi_select: Allow multiple selections
"""
self.title = title
self.options = options
self.multi_select = multi_select
self.selected = set() if multi_select else None
def display(self) -> Union[int, List[int]]:
"""
Display menu and get user selection
Returns:
Selected option index (single) or list of indices (multi-select)
"""
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{self.title}{Colors.RESET}")
print("=" * len(self.title))
for i, option in enumerate(self.options, 1):
if self.multi_select:
marker = "[x]" if i - 1 in (self.selected or set()) else "[ ]"
print(f"{Colors.YELLOW}{i:2d}.{Colors.RESET} {marker} {option}")
else:
print(f"{Colors.YELLOW}{i:2d}.{Colors.RESET} {option}")
if self.multi_select:
print(
f"\n{Colors.BLUE}Enter numbers separated by commas (e.g., 1,3,5) or 'all' for all options:{Colors.RESET}"
)
else:
print(
f"\n{Colors.BLUE}Enter your choice (1-{len(self.options)}):{Colors.RESET}"
)
while True:
try:
user_input = input("> ").strip().lower()
if self.multi_select:
if user_input == "all":
return list(range(len(self.options)))
elif user_input == "":
return []
else:
# Parse comma-separated numbers
selections = []
for part in user_input.split(","):
part = part.strip()
if part.isdigit():
idx = int(part) - 1
if 0 <= idx < len(self.options):
selections.append(idx)
else:
raise ValueError(f"Invalid option: {part}")
else:
raise ValueError(f"Invalid input: {part}")
return list(set(selections)) # Remove duplicates
else:
if user_input.isdigit():
choice = int(user_input) - 1
if 0 <= choice < len(self.options):
return choice
else:
print(
f"{Colors.RED}Invalid choice. Please enter a number between 1 and {len(self.options)}.{Colors.RESET}"
)
else:
print(f"{Colors.RED}Please enter a valid number.{Colors.RESET}")
except (ValueError, KeyboardInterrupt) as e:
if isinstance(e, KeyboardInterrupt):
print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
return [] if self.multi_select else -1
else:
print(f"{Colors.RED}Invalid input: {e}{Colors.RESET}")
def confirm(message: str, default: bool = True) -> bool:
"""
Ask for user confirmation
Args:
message: Confirmation message
default: Default response if user just presses Enter
Returns:
True if confirmed, False otherwise
"""
suffix = "[Y/n]" if default else "[y/N]"
print(f"{Colors.BLUE}{message} {suffix}{Colors.RESET}")
while True:
try:
response = input("> ").strip().lower()
if response == "":
return default
elif response in ["y", "yes", "true", "1"]:
return True
elif response in ["n", "no", "false", "0"]:
return False
else:
print(
f"{Colors.RED}Please enter 'y' or 'n' (or press Enter for default).{Colors.RESET}"
)
except KeyboardInterrupt:
print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
return False
def display_header(title: str, subtitle: str = "") -> None:
"""
Display formatted header
Args:
title: Main title
subtitle: Optional subtitle
"""
from superclaude import __author__, __email__
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{title:^60}{Colors.RESET}")
if subtitle:
print(f"{Colors.WHITE}{subtitle:^60}{Colors.RESET}")
# Display authors
authors = [a.strip() for a in __author__.split(",")]
emails = [e.strip() for e in __email__.split(",")]
author_lines = []
for i in range(len(authors)):
name = authors[i]
email = emails[i] if i < len(emails) else ""
author_lines.append(f"{name} <{email}>")
authors_str = " | ".join(author_lines)
print(f"{Colors.BLUE}{authors_str:^60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}\n")
def display_authors() -> None:
"""Display author information"""
from superclaude import __author__, __email__, __github__
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'superclaude Authors':^60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}\n")
authors = [a.strip() for a in __author__.split(",")]
emails = [e.strip() for e in __email__.split(",")]
github_users = [g.strip() for g in __github__.split(",")]
for i in range(len(authors)):
name = authors[i]
email = emails[i] if i < len(emails) else "N/A"
github = github_users[i] if i < len(github_users) else "N/A"
print(f" {Colors.BRIGHT}{name}{Colors.RESET}")
print(f" Email: {Colors.YELLOW}{email}{Colors.RESET}")
print(f" GitHub: {Colors.YELLOW}https://github.com/{github}{Colors.RESET}")
print()
print(f"{Colors.CYAN}{'='*60}{Colors.RESET}\n")
def display_info(message: str) -> None:
"""Display info message"""
print(f"{Colors.BLUE}[INFO] {message}{Colors.RESET}")
def display_success(message: str) -> None:
"""Display success message"""
safe_print(f"{Colors.GREEN}[{symbols.checkmark}] {message}{Colors.RESET}")
def display_warning(message: str) -> None:
"""Display warning message"""
print(f"{Colors.YELLOW}[!] {message}{Colors.RESET}")
def display_error(message: str) -> None:
"""Display error message"""
safe_print(f"{Colors.RED}[{symbols.crossmark}] {message}{Colors.RESET}")
def display_step(step: int, total: int, message: str) -> None:
"""Display step progress"""
print(f"{Colors.CYAN}[{step}/{total}] {message}{Colors.RESET}")
def display_table(headers: List[str], rows: List[List[str]], title: str = "") -> None:
"""
Display data in table format
Args:
headers: Column headers
rows: Data rows
title: Optional table title
"""
if not rows:
return
# Calculate column widths
col_widths = [len(header) for header in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(str(cell)))
# Display title
if title:
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{title}{Colors.RESET}")
print()
# Display headers
header_line = " | ".join(
f"{header:<{col_widths[i]}}" for i, header in enumerate(headers)
)
print(f"{Colors.YELLOW}{header_line}{Colors.RESET}")
print("-" * len(header_line))
# Display rows
for row in rows:
row_line = " | ".join(
f"{str(cell):<{col_widths[i]}}" for i, cell in enumerate(row)
)
print(row_line)
print()
def prompt_api_key(service_name: str, env_var_name: str) -> Optional[str]:
"""
Prompt for API key with security and UX best practices
Args:
service_name: Human-readable service name (e.g., "Magic", "Morphllm")
env_var_name: Environment variable name (e.g., "TWENTYFIRST_API_KEY")
Returns:
API key string if provided, None if skipped
"""
print(
f"{Colors.BLUE}[API KEY] {service_name} requires: {Colors.BRIGHT}{env_var_name}{Colors.RESET}"
)
print(
f"{Colors.WHITE}Visit the service documentation to obtain your API key{Colors.RESET}"
)
print(
f"{Colors.YELLOW}Press Enter to skip (you can set this manually later){Colors.RESET}"
)
try:
# Use getpass for hidden input
api_key = getpass.getpass(f"Enter {env_var_name}: ").strip()
if not api_key:
print(
f"{Colors.YELLOW}[SKIPPED] {env_var_name} - set manually later{Colors.RESET}"
)
return None
# Basic validation (non-empty, reasonable length)
if len(api_key) < 10:
print(
f"{Colors.RED}[WARNING] API key seems too short. Continue anyway? (y/N){Colors.RESET}"
)
if not confirm("", default=False):
return None
safe_print(
f"{Colors.GREEN}[{symbols.checkmark}] {env_var_name} configured{Colors.RESET}"
)
return api_key
except KeyboardInterrupt:
safe_print(f"\n{Colors.YELLOW}[SKIPPED] {env_var_name}{Colors.RESET}")
return None
def wait_for_key(message: str = "Press Enter to continue...") -> None:
"""Wait for user to press a key"""
try:
input(f"{Colors.BLUE}{message}{Colors.RESET}")
except KeyboardInterrupt:
print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
def clear_screen() -> None:
"""Clear terminal screen"""
import os
os.system("cls" if os.name == "nt" else "clear")
class StatusSpinner:
"""Simple status spinner for long operations"""
def __init__(self, message: str = "Working..."):
"""
Initialize spinner
Args:
message: Message to display with spinner
"""
self.message = message
self.spinning = False
self.chars = symbols.spinner_chars
self.current = 0
def start(self) -> None:
"""Start spinner in background thread"""
import threading
def spin():
while self.spinning:
char = self.chars[self.current % len(self.chars)]
safe_print(
f"\r{Colors.BLUE}{char} {self.message}{Colors.RESET}",
end="",
flush=True,
)
self.current += 1
time.sleep(0.1)
self.spinning = True
self.thread = threading.Thread(target=spin, daemon=True)
self.thread.start()
def stop(self, final_message: str = "") -> None:
"""
Stop spinner
Args:
final_message: Final message to display
"""
self.spinning = False
if hasattr(self, "thread"):
self.thread.join(timeout=0.2)
# Clear spinner line
safe_print(f"\r{' ' * (len(self.message) + 5)}\r", end="")
if final_message:
safe_print(final_message)
def format_size(size_bytes: int) -> str:
"""Format file size in human-readable format"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} PB"
def format_duration(seconds: float) -> str:
"""Format duration in human-readable format"""
if seconds < 1:
return f"{seconds*1000:.0f}ms"
elif seconds < 60:
return f"{seconds:.1f}s"
elif seconds < 3600:
minutes = seconds // 60
secs = seconds % 60
return f"{minutes:.0f}m {secs:.0f}s"
else:
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return f"{hours:.0f}h {minutes:.0f}m"
def truncate_text(text: str, max_length: int, suffix: str = "...") -> str:
"""Truncate text to maximum length with optional suffix"""
if len(text) <= max_length:
return text
return text[: max_length - len(suffix)] + suffix