mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-29 16:16:08 +00:00
refactor: PEP8 compliance - directory rename and code formatting (#425)
* fix(orchestration): add WebFetch auto-trigger for infrastructure configuration Problem: Infrastructure configuration changes (e.g., Traefik port settings) were being made based on assumptions without consulting official documentation, violating the 'Evidence > assumptions' principle in PRINCIPLES.md. Solution: - Added Infrastructure Configuration Validation section to MODE_Orchestration.md - Auto-triggers WebFetch for infrastructure tools (Traefik, nginx, Docker, etc.) - Enforces MODE_DeepResearch activation for investigation - BLOCKS assumption-based configuration changes Testing: Verified WebFetch successfully retrieves Traefik official docs (port 80 default) This prevents production outages from infrastructure misconfiguration by ensuring all technical recommendations are backed by official documentation. * feat: Add PM Agent (Project Manager Agent) for seamless orchestration Introduces PM Agent as the default orchestration layer that coordinates all sub-agents and manages workflows automatically. Key Features: - Default orchestration: All user interactions handled by PM Agent - Auto-delegation: Intelligent sub-agent selection based on task analysis - Docker Gateway integration: Zero-token baseline with dynamic MCP loading - Self-improvement loop: Automatic documentation of patterns and mistakes - Optional override: Users can specify sub-agents explicitly if desired Architecture: - Agent spec: SuperClaude/Agents/pm-agent.md - Command: SuperClaude/Commands/pm.md - Updated docs: README.md (15→16 agents), agents.md (new Orchestration category) User Experience: - Default: PM Agent handles everything (seamless, no manual routing) - Optional: Explicit --agent flag for direct sub-agent access - Both modes available simultaneously (no user downside) Implementation Status: - ✅ Specification complete - ✅ Documentation complete - ⏳ Prototype implementation needed - ⏳ Docker Gateway integration needed - ⏳ Testing and validation needed Refs: kazukinakai/docker-mcp-gateway (IRIS MCP Gateway integration) * feat: Add Agent Orchestration rules for PM Agent default activation Implements PM Agent as the default orchestration layer in RULES.md. Key Changes: - New 'Agent Orchestration' section (CRITICAL priority) - PM Agent receives ALL user requests by default - Manual override with @agent-[name] bypasses PM Agent - Agent Selection Priority clearly defined: 1. Manual override → Direct routing 2. Default → PM Agent → Auto-delegation 3. Delegation based on keywords, file types, complexity, context User Experience: - Default: PM Agent handles everything (seamless) - Override: @agent-[name] for direct specialist access - Transparent: PM Agent reports delegation decisions This establishes PM Agent as the orchestration layer while respecting existing auto-activation patterns and manual overrides. Next Steps: - Local testing in agiletec project - Iteration based on actual behavior - Documentation updates as needed * refactor(pm-agent): redesign as self-improvement meta-layer Problem Resolution: PM Agent's initial design competed with existing auto-activation for task routing, creating confusion about orchestration responsibilities and adding unnecessary complexity. Design Change: Redefined PM Agent as a meta-layer agent that operates AFTER specialist agents complete tasks, focusing on: - Post-implementation documentation and pattern recording - Immediate mistake analysis with prevention checklists - Monthly documentation maintenance and noise reduction - Pattern extraction and knowledge synthesis Two-Layer Orchestration System: 1. Task Execution Layer: Existing auto-activation handles task routing (unchanged) 2. Self-Improvement Layer: PM Agent meta-layer handles documentation (new) Files Modified: - SuperClaude/Agents/pm-agent.md: Complete rewrite with meta-layer design - Category: orchestration → meta - Triggers: All user interactions → Post-implementation, mistakes, monthly - Behavioral Mindset: Continuous learning system - Self-Improvement Workflow: BEFORE/DURING/AFTER/MISTAKE RECOVERY/MAINTENANCE - SuperClaude/Core/RULES.md: Agent Orchestration section updated - Split into Task Execution Layer + Self-Improvement Layer - Added orchestration flow diagram - Clarified PM Agent activates AFTER task completion - README.md: Updated PM Agent description - "orchestrates all interactions" → "ensures continuous learning" - Docs/User-Guide/agents.md: PM Agent section rewritten - Section: Orchestration Agent → Meta-Layer Agent - Expertise: Project orchestration → Self-improvement workflow executor - Examples: Task coordination → Post-implementation documentation - PR_DOCUMENTATION.md: Comprehensive PR documentation added - Summary, motivation, changes, testing, breaking changes - Two-layer orchestration system diagram - Verification checklist Integration Validated: Tested with agiletec project's self-improvement-workflow.md: ✅ PM Agent aligns with existing BEFORE/DURING/AFTER/MISTAKE RECOVERY phases ✅ Complements (not competes with) existing workflow ✅ agiletec workflow defines WHAT, PM Agent defines WHO executes it Breaking Changes: None - Existing auto-activation continues unchanged - Specialist agents unaffected - User workflows remain the same - New capability: Automatic documentation and knowledge maintenance Value Proposition: Transforms SuperClaude into a continuously learning system that accumulates knowledge, prevents recurring mistakes, and maintains fresh documentation without manual intervention. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add Claude Code conversation history management research Research covering .jsonl file structure, performance impact, and retention policies. Content: - Claude Code .jsonl file format and message types - Performance issues from GitHub (memory leaks, conversation compaction) - Retention policies (consumer vs enterprise) - Rotation recommendations based on actual data - File history snapshot tracking mechanics Source: Moved from agiletec project (research applicable to all Claude Code projects) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add Development documentation structure Phase 1: Documentation Structure complete - Add Docs/Development/ directory for development documentation - Add ARCHITECTURE.md - System architecture with PM Agent meta-layer - Add ROADMAP.md - 5-phase development plan with checkboxes - Add TASKS.md - Daily task tracking with progress indicators - Add PROJECT_STATUS.md - Current status dashboard and metrics - Add pm-agent-integration.md - Implementation guide for PM Agent mode This establishes comprehensive documentation foundation for: - System architecture understanding - Development planning and tracking - Implementation guidance - Progress visibility Related: #pm-agent-mode #documentation #phase-1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: PM Agent session lifecycle and PDCA implementation Phase 2: PM Agent Mode Integration (Design Phase) Commands/pm.md updates: - Add "Always-Active Foundation Layer" concept - Add Session Lifecycle (Session Start/During Work/Session End) - Add PDCA Cycle (Plan/Do/Check/Act) automation - Add Serena MCP Memory Integration (list/read/write_memory) - Document auto-activation triggers Agents/pm-agent.md updates: - Add Session Start Protocol (MANDATORY auto-activation) - Add During Work PDCA Cycle with example workflows - Add Session End Protocol with state preservation - Add PDCA Self-Evaluation Pattern - Add Documentation Strategy (temp → patterns/mistakes) - Add Memory Operations Reference Key Features: - Session start auto-activation for context restoration - 30-minute checkpoint saves during work - Self-evaluation with think_about_* operations - Systematic documentation lifecycle - Knowledge evolution to CLAUDE.md Implementation Status: - ✅ Design complete (Commands/pm.md, Agents/pm-agent.md) - ⏳ Implementation pending (Core components) - ⏳ Serena MCP integration pending Salvaged from mistaken development in ~/.claude directory Related: #pm-agent-mode #session-lifecycle #pdca-cycle #phase-2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: disable Serena MCP auto-browser launch Disable web dashboard and GUI log window auto-launch in Serena MCP server to prevent intrusive browser popups on startup. Users can still manually access the dashboard at http://localhost:24282/dashboard/ if needed. Changes: - Add CLI flags to Serena run command: - --enable-web-dashboard false - --enable-gui-log-window false - Ensures Git-tracked configuration (no reliance on ~/.serena/serena_config.yml) - Aligns with AIRIS MCP Gateway integration approach 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rename directories to lowercase for PEP8 compliance - Rename superclaude/Agents -> superclaude/agents - Rename superclaude/Commands -> superclaude/commands - Rename superclaude/Core -> superclaude/core - Rename superclaude/Examples -> superclaude/examples - Rename superclaude/MCP -> superclaude/mcp - Rename superclaude/Modes -> superclaude/modes This change follows Python PEP8 naming conventions for package directories. * style: fix PEP8 violations and update package name to lowercase Changes: - Format all Python files with black (43 files reformatted) - Update package name from 'SuperClaude' to 'superclaude' in pyproject.toml - Fix import statements to use lowercase package name - Add missing imports (timedelta, __version__) - Remove old SuperClaude.egg-info directory PEP8 violations reduced from 2672 to 701 (mostly E501 line length due to black's 88 char vs flake8's 79 char limit). * docs: add PM Agent development documentation Add comprehensive PM Agent development documentation: - PM Agent ideal workflow (7-phase autonomous cycle) - Project structure understanding (Git vs installed environment) - Installation flow understanding (CommandsComponent behavior) - Task management system (current-tasks.md) Purpose: Eliminate repeated explanations and enable autonomous PDCA cycles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(pm-agent): add self-correcting execution and warning investigation culture ## Changes ### superclaude/commands/pm.md - Add "Self-Correcting Execution" section with root cause analysis protocol - Add "Warning/Error Investigation Culture" section enforcing zero-tolerance for dismissal - Define error detection protocol: STOP → Investigate → Hypothesis → Different Solution → Execute - Document anti-patterns (retry without understanding) and correct patterns (research-first) ### docs/Development/hypothesis-pm-autonomous-enhancement-2025-10-14.md - Add PDCA workflow hypothesis document for PM Agent autonomous enhancement ## Rationale PM Agent must never retry failed operations without understanding root causes. All warnings and errors require investigation via context7/WebFetch/documentation to ensure production-quality code and prevent technical debt accumulation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(installer): add airis-mcp-gateway MCP server option ## Changes - Add airis-mcp-gateway to MCP server options in installer - Configuration: GitHub-based installation via uvx - Repository: https://github.com/oraios/airis-mcp-gateway - Purpose: Dynamic MCP Gateway for zero-token baseline and on-demand tool loading ## Implementation Added to setup/components/mcp.py self.mcp_servers dictionary with: - install_method: github - install_command: uvx test installation - run_command: uvx runtime execution - required: False (optional server) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: kazuki <kazuki@kazukinoMacBook-Air.local> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,5 +7,5 @@ from .base import OperationBase
|
||||
from .commands import *
|
||||
|
||||
__all__ = [
|
||||
'OperationBase',
|
||||
]
|
||||
"OperationBase",
|
||||
]
|
||||
|
||||
@@ -19,61 +19,65 @@ def get_command_info():
|
||||
"install": {
|
||||
"name": "install",
|
||||
"description": "Install SuperClaude framework components",
|
||||
"module": "setup.cli.commands.install"
|
||||
"module": "setup.cli.commands.install",
|
||||
},
|
||||
"update": {
|
||||
"name": "update",
|
||||
"name": "update",
|
||||
"description": "Update existing SuperClaude installation",
|
||||
"module": "setup.cli.commands.update"
|
||||
"module": "setup.cli.commands.update",
|
||||
},
|
||||
"uninstall": {
|
||||
"name": "uninstall",
|
||||
"description": "Remove SuperClaude framework installation",
|
||||
"module": "setup.cli.commands.uninstall"
|
||||
"description": "Remove SuperClaude framework installation",
|
||||
"module": "setup.cli.commands.uninstall",
|
||||
},
|
||||
"backup": {
|
||||
"name": "backup",
|
||||
"description": "Backup and restore SuperClaude installations",
|
||||
"module": "setup.cli.commands.backup"
|
||||
}
|
||||
"module": "setup.cli.commands.backup",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OperationBase:
|
||||
"""Base class for all operations providing common functionality"""
|
||||
|
||||
|
||||
def __init__(self, operation_name: str):
|
||||
self.operation_name = operation_name
|
||||
self.logger = None
|
||||
|
||||
|
||||
def setup_operation_logging(self, args):
|
||||
"""Setup operation-specific logging"""
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
self.logger = get_logger()
|
||||
self.logger.info(f"Starting {self.operation_name} operation")
|
||||
|
||||
|
||||
def validate_global_args(self, args):
|
||||
"""Validate global arguments common to all operations"""
|
||||
errors = []
|
||||
|
||||
|
||||
# Validate install directory
|
||||
if hasattr(args, 'install_dir') and args.install_dir:
|
||||
if hasattr(args, "install_dir") and args.install_dir:
|
||||
from ..utils.security import SecurityValidator
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(args.install_dir)
|
||||
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(
|
||||
args.install_dir
|
||||
)
|
||||
if not is_safe:
|
||||
errors.extend(validation_errors)
|
||||
|
||||
|
||||
# Check for conflicting flags
|
||||
if hasattr(args, 'verbose') and hasattr(args, 'quiet'):
|
||||
if hasattr(args, "verbose") and hasattr(args, "quiet"):
|
||||
if args.verbose and args.quiet:
|
||||
errors.append("Cannot specify both --verbose and --quiet")
|
||||
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
def handle_operation_error(self, operation: str, error: Exception):
|
||||
"""Standard error handling for operations"""
|
||||
if self.logger:
|
||||
self.logger.exception(f"Error in {operation} operation: {error}")
|
||||
else:
|
||||
print(f"Error in {operation} operation: {error}")
|
||||
return 1
|
||||
return 1
|
||||
|
||||
@@ -10,9 +10,9 @@ from .update import UpdateOperation
|
||||
from .backup import BackupOperation
|
||||
|
||||
__all__ = [
|
||||
'OperationBase',
|
||||
'InstallOperation',
|
||||
'UninstallOperation',
|
||||
'UpdateOperation',
|
||||
'BackupOperation'
|
||||
]
|
||||
"OperationBase",
|
||||
"InstallOperation",
|
||||
"UninstallOperation",
|
||||
"UpdateOperation",
|
||||
"BackupOperation",
|
||||
]
|
||||
|
||||
@@ -9,14 +9,22 @@ import tarfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from ...utils.paths import get_home_directory
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
import argparse
|
||||
|
||||
from ...services.settings import SettingsService
|
||||
from ...utils.ui import (
|
||||
display_header, display_info, display_success, display_error,
|
||||
display_warning, Menu, confirm, ProgressBar, Colors, format_size
|
||||
display_header,
|
||||
display_info,
|
||||
display_success,
|
||||
display_error,
|
||||
display_warning,
|
||||
Menu,
|
||||
confirm,
|
||||
ProgressBar,
|
||||
Colors,
|
||||
format_size,
|
||||
)
|
||||
from ...utils.logger import get_logger
|
||||
from ... import DEFAULT_INSTALL_DIR
|
||||
@@ -25,7 +33,7 @@ from . import OperationBase
|
||||
|
||||
class BackupOperation(OperationBase):
|
||||
"""Backup operation implementation"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("backup")
|
||||
|
||||
@@ -33,7 +41,7 @@ class BackupOperation(OperationBase):
|
||||
def register_parser(subparsers, global_parser=None) -> argparse.ArgumentParser:
|
||||
"""Register backup CLI arguments"""
|
||||
parents = [global_parser] if global_parser else []
|
||||
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"backup",
|
||||
help="Backup and restore SuperClaude installations",
|
||||
@@ -48,84 +56,70 @@ Examples:
|
||||
SuperClaude backup --cleanup --force # Clean up old backups (forced)
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=parents
|
||||
parents=parents,
|
||||
)
|
||||
|
||||
|
||||
# Backup operations (mutually exclusive)
|
||||
operation_group = parser.add_mutually_exclusive_group(required=True)
|
||||
|
||||
|
||||
operation_group.add_argument(
|
||||
"--create",
|
||||
action="store_true",
|
||||
help="Create a new backup"
|
||||
"--create", action="store_true", help="Create a new backup"
|
||||
)
|
||||
|
||||
|
||||
operation_group.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available backups"
|
||||
"--list", action="store_true", help="List available backups"
|
||||
)
|
||||
|
||||
|
||||
operation_group.add_argument(
|
||||
"--restore",
|
||||
nargs="?",
|
||||
const="interactive",
|
||||
help="Restore from backup (optionally specify backup file)"
|
||||
help="Restore from backup (optionally specify backup file)",
|
||||
)
|
||||
|
||||
|
||||
operation_group.add_argument(
|
||||
"--info",
|
||||
type=str,
|
||||
help="Show information about a specific backup file"
|
||||
"--info", type=str, help="Show information about a specific backup file"
|
||||
)
|
||||
|
||||
|
||||
operation_group.add_argument(
|
||||
"--cleanup",
|
||||
action="store_true",
|
||||
help="Clean up old backup files"
|
||||
"--cleanup", action="store_true", help="Clean up old backup files"
|
||||
)
|
||||
|
||||
|
||||
# Backup options
|
||||
parser.add_argument(
|
||||
"--backup-dir",
|
||||
type=Path,
|
||||
help="Backup directory (default: <install-dir>/backups)"
|
||||
help="Backup directory (default: <install-dir>/backups)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
type=str,
|
||||
help="Custom backup name (for --create)"
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument("--name", type=str, help="Custom backup name (for --create)")
|
||||
|
||||
parser.add_argument(
|
||||
"--compress",
|
||||
choices=["none", "gzip", "bzip2"],
|
||||
default="gzip",
|
||||
help="Compression method (default: gzip)"
|
||||
help="Compression method (default: gzip)",
|
||||
)
|
||||
|
||||
|
||||
# Restore options
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite existing files during restore"
|
||||
help="Overwrite existing files during restore",
|
||||
)
|
||||
|
||||
|
||||
# Cleanup options
|
||||
parser.add_argument(
|
||||
"--keep",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of backups to keep during cleanup (default: 5)"
|
||||
help="Number of backups to keep during cleanup (default: 5)",
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"--older-than",
|
||||
type=int,
|
||||
help="Remove backups older than N days"
|
||||
"--older-than", type=int, help="Remove backups older than N days"
|
||||
)
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -141,7 +135,10 @@ def check_installation_exists(install_dir: Path) -> bool:
|
||||
"""Check if SuperClaude installation (v2 included) exists"""
|
||||
settings_manager = SettingsService(install_dir)
|
||||
|
||||
return settings_manager.check_installation_exists() or settings_manager.check_v2_installation_exists()
|
||||
return (
|
||||
settings_manager.check_installation_exists()
|
||||
or settings_manager.check_v2_installation_exists()
|
||||
)
|
||||
|
||||
|
||||
def get_backup_info(backup_path: Path) -> Dict[str, Any]:
|
||||
@@ -151,18 +148,18 @@ def get_backup_info(backup_path: Path) -> Dict[str, Any]:
|
||||
"exists": backup_path.exists(),
|
||||
"size": 0,
|
||||
"created": None,
|
||||
"metadata": {}
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
|
||||
if not backup_path.exists():
|
||||
return info
|
||||
|
||||
|
||||
try:
|
||||
# Get file stats
|
||||
stats = backup_path.stat()
|
||||
info["size"] = stats.st_size
|
||||
info["created"] = datetime.fromtimestamp(stats.st_mtime)
|
||||
|
||||
|
||||
# Try to read metadata from backup
|
||||
if backup_path.suffix == ".gz":
|
||||
mode = "r:gz"
|
||||
@@ -170,7 +167,7 @@ def get_backup_info(backup_path: Path) -> Dict[str, Any]:
|
||||
mode = "r:bz2"
|
||||
else:
|
||||
mode = "r"
|
||||
|
||||
|
||||
with tarfile.open(backup_path, mode) as tar:
|
||||
# Look for metadata file
|
||||
try:
|
||||
@@ -180,32 +177,32 @@ def get_backup_info(backup_path: Path) -> Dict[str, Any]:
|
||||
info["metadata"] = json.loads(metadata_file.read().decode())
|
||||
except KeyError:
|
||||
pass # No metadata file
|
||||
|
||||
|
||||
# Get list of files in backup
|
||||
info["files"] = len(tar.getnames())
|
||||
|
||||
|
||||
except Exception as e:
|
||||
info["error"] = str(e)
|
||||
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def list_backups(backup_dir: Path) -> List[Dict[str, Any]]:
|
||||
"""List all available backups"""
|
||||
backups = []
|
||||
|
||||
|
||||
if not backup_dir.exists():
|
||||
return backups
|
||||
|
||||
|
||||
# Find all backup files
|
||||
for backup_file in backup_dir.glob("*.tar*"):
|
||||
if backup_file.is_file():
|
||||
info = get_backup_info(backup_file)
|
||||
backups.append(info)
|
||||
|
||||
|
||||
# Sort by creation date (newest first)
|
||||
backups.sort(key=lambda x: x.get("created", datetime.min), reverse=True)
|
||||
|
||||
|
||||
return backups
|
||||
|
||||
|
||||
@@ -213,43 +210,49 @@ def display_backup_list(backups: List[Dict[str, Any]]) -> None:
|
||||
"""Display list of available backups"""
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}Available Backups{Colors.RESET}")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if not backups:
|
||||
print(f"{Colors.YELLOW}No backups found{Colors.RESET}")
|
||||
return
|
||||
|
||||
|
||||
print(f"{'Name':<30} {'Size':<10} {'Created':<20} {'Files':<8}")
|
||||
print("-" * 70)
|
||||
|
||||
|
||||
for backup in backups:
|
||||
name = backup["path"].name
|
||||
size = format_size(backup["size"]) if backup["size"] > 0 else "unknown"
|
||||
created = backup["created"].strftime("%Y-%m-%d %H:%M") if backup["created"] else "unknown"
|
||||
created = (
|
||||
backup["created"].strftime("%Y-%m-%d %H:%M")
|
||||
if backup["created"]
|
||||
else "unknown"
|
||||
)
|
||||
files = str(backup.get("files", "unknown"))
|
||||
|
||||
|
||||
print(f"{name:<30} {size:<10} {created:<20} {files:<8}")
|
||||
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def create_backup_metadata(install_dir: Path) -> Dict[str, Any]:
|
||||
"""Create metadata for the backup"""
|
||||
from setup import __version__
|
||||
|
||||
metadata = {
|
||||
"backup_version": __version__,
|
||||
"created": datetime.now().isoformat(),
|
||||
"install_dir": str(install_dir),
|
||||
"components": {},
|
||||
"framework_version": "unknown"
|
||||
"framework_version": "unknown",
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
# Get installed components from metadata
|
||||
settings_manager = SettingsService(install_dir)
|
||||
framework_config = settings_manager.get_metadata_setting("framework")
|
||||
|
||||
|
||||
if framework_config:
|
||||
metadata["framework_version"] = framework_config.get("version", "unknown")
|
||||
|
||||
|
||||
if "components" in framework_config:
|
||||
for component_name in framework_config["components"]:
|
||||
version = settings_manager.get_component_version(component_name)
|
||||
@@ -257,31 +260,31 @@ def create_backup_metadata(install_dir: Path) -> Dict[str, Any]:
|
||||
metadata["components"][component_name] = version
|
||||
except Exception:
|
||||
pass # Continue without metadata
|
||||
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def create_backup(args: argparse.Namespace) -> bool:
|
||||
"""Create a new backup"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
# Check if installation exists
|
||||
if not check_installation_exists(args.install_dir):
|
||||
logger.error(f"No SuperClaude installation found in {args.install_dir}")
|
||||
return False
|
||||
|
||||
|
||||
# Setup backup directory
|
||||
backup_dir = get_backup_directory(args)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Generate backup filename
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
if args.name:
|
||||
backup_name = f"{args.name}_{timestamp}"
|
||||
else:
|
||||
backup_name = f"superclaude_backup_{timestamp}"
|
||||
|
||||
|
||||
# Determine compression
|
||||
if args.compress == "gzip":
|
||||
backup_file = backup_dir / f"{backup_name}.tar.gz"
|
||||
@@ -292,24 +295,27 @@ def create_backup(args: argparse.Namespace) -> bool:
|
||||
else:
|
||||
backup_file = backup_dir / f"{backup_name}.tar"
|
||||
mode = "w"
|
||||
|
||||
|
||||
logger.info(f"Creating backup: {backup_file}")
|
||||
|
||||
|
||||
# Create metadata
|
||||
metadata = create_backup_metadata(args.install_dir)
|
||||
|
||||
|
||||
# Create backup
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
with tarfile.open(backup_file, mode) as tar:
|
||||
# Add metadata file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as temp_file:
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False
|
||||
) as temp_file:
|
||||
json.dump(metadata, temp_file, indent=2)
|
||||
temp_file.flush()
|
||||
tar.add(temp_file.name, arcname="backup_metadata.json")
|
||||
Path(temp_file.name).unlink() # Clean up temp file
|
||||
|
||||
|
||||
# Add installation directory contents (excluding backups and local dirs)
|
||||
files_added = 0
|
||||
for item in args.install_dir.rglob("*"):
|
||||
@@ -317,30 +323,30 @@ def create_backup(args: argparse.Namespace) -> bool:
|
||||
try:
|
||||
# Create relative path for archive
|
||||
rel_path = item.relative_to(args.install_dir)
|
||||
|
||||
|
||||
# Skip files in excluded directories
|
||||
if rel_path.parts and rel_path.parts[0] in ["backups", "local"]:
|
||||
continue
|
||||
|
||||
|
||||
tar.add(item, arcname=str(rel_path))
|
||||
files_added += 1
|
||||
|
||||
|
||||
if files_added % 10 == 0:
|
||||
logger.debug(f"Added {files_added} files to backup")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not add {item} to backup: {e}")
|
||||
|
||||
|
||||
duration = time.time() - start_time
|
||||
file_size = backup_file.stat().st_size
|
||||
|
||||
|
||||
logger.success(f"Backup created successfully in {duration:.1f} seconds")
|
||||
logger.info(f"Backup file: {backup_file}")
|
||||
logger.info(f"Files archived: {files_added}")
|
||||
logger.info(f"Backup size: {format_size(file_size)}")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to create backup: {e}")
|
||||
return False
|
||||
@@ -349,20 +355,20 @@ def create_backup(args: argparse.Namespace) -> bool:
|
||||
def restore_backup(backup_path: Path, args: argparse.Namespace) -> bool:
|
||||
"""Restore from a backup file"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
if not backup_path.exists():
|
||||
logger.error(f"Backup file not found: {backup_path}")
|
||||
return False
|
||||
|
||||
|
||||
# Check backup file
|
||||
info = get_backup_info(backup_path)
|
||||
if "error" in info:
|
||||
logger.error(f"Invalid backup file: {info['error']}")
|
||||
return False
|
||||
|
||||
|
||||
logger.info(f"Restoring from backup: {backup_path}")
|
||||
|
||||
|
||||
# Determine compression
|
||||
if backup_path.suffix == ".gz":
|
||||
mode = "r:gz"
|
||||
@@ -370,47 +376,47 @@ def restore_backup(backup_path: Path, args: argparse.Namespace) -> bool:
|
||||
mode = "r:bz2"
|
||||
else:
|
||||
mode = "r"
|
||||
|
||||
|
||||
# Create backup of current installation if it exists
|
||||
if check_installation_exists(args.install_dir) and not args.dry_run:
|
||||
logger.info("Creating backup of current installation before restore")
|
||||
# This would call create_backup internally
|
||||
|
||||
|
||||
# Extract backup
|
||||
start_time = time.time()
|
||||
files_restored = 0
|
||||
|
||||
|
||||
with tarfile.open(backup_path, mode) as tar:
|
||||
# Extract all files except metadata
|
||||
for member in tar.getmembers():
|
||||
if member.name == "backup_metadata.json":
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
target_path = args.install_dir / member.name
|
||||
|
||||
|
||||
# Check if file exists and overwrite flag
|
||||
if target_path.exists() and not args.overwrite:
|
||||
logger.warning(f"Skipping existing file: {target_path}")
|
||||
continue
|
||||
|
||||
|
||||
# Extract file
|
||||
tar.extract(member, args.install_dir)
|
||||
files_restored += 1
|
||||
|
||||
|
||||
if files_restored % 10 == 0:
|
||||
logger.debug(f"Restored {files_restored} files")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not restore {member.name}: {e}")
|
||||
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
|
||||
logger.success(f"Restore completed successfully in {duration:.1f} seconds")
|
||||
logger.info(f"Files restored: {files_restored}")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to restore backup: {e}")
|
||||
return False
|
||||
@@ -421,69 +427,73 @@ def interactive_restore_selection(backups: List[Dict[str, Any]]) -> Optional[Pat
|
||||
if not backups:
|
||||
print(f"{Colors.YELLOW}No backups available for restore{Colors.RESET}")
|
||||
return None
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}Select Backup to Restore:{Colors.RESET}")
|
||||
|
||||
|
||||
# Create menu options
|
||||
backup_options = []
|
||||
for backup in backups:
|
||||
name = backup["path"].name
|
||||
size = format_size(backup["size"]) if backup["size"] > 0 else "unknown"
|
||||
created = backup["created"].strftime("%Y-%m-%d %H:%M") if backup["created"] else "unknown"
|
||||
created = (
|
||||
backup["created"].strftime("%Y-%m-%d %H:%M")
|
||||
if backup["created"]
|
||||
else "unknown"
|
||||
)
|
||||
backup_options.append(f"{name} ({size}, {created})")
|
||||
|
||||
|
||||
menu = Menu("Select backup:", backup_options)
|
||||
choice = menu.display()
|
||||
|
||||
|
||||
if choice == -1 or choice >= len(backups):
|
||||
return None
|
||||
|
||||
|
||||
return backups[choice]["path"]
|
||||
|
||||
|
||||
def cleanup_old_backups(backup_dir: Path, args: argparse.Namespace) -> bool:
|
||||
"""Clean up old backup files"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
backups = list_backups(backup_dir)
|
||||
if not backups:
|
||||
logger.info("No backups found to clean up")
|
||||
return True
|
||||
|
||||
|
||||
to_remove = []
|
||||
|
||||
|
||||
# Remove by age
|
||||
if args.older_than:
|
||||
cutoff_date = datetime.now() - timedelta(days=args.older_than)
|
||||
for backup in backups:
|
||||
if backup["created"] and backup["created"] < cutoff_date:
|
||||
to_remove.append(backup)
|
||||
|
||||
|
||||
# Keep only N most recent
|
||||
if args.keep and len(backups) > args.keep:
|
||||
# Sort by date and take oldest ones to remove
|
||||
backups.sort(key=lambda x: x.get("created", datetime.min), reverse=True)
|
||||
to_remove.extend(backups[args.keep:])
|
||||
|
||||
to_remove.extend(backups[args.keep :])
|
||||
|
||||
# Remove duplicates
|
||||
to_remove = list({backup["path"]: backup for backup in to_remove}.values())
|
||||
|
||||
|
||||
if not to_remove:
|
||||
logger.info("No backups need to be cleaned up")
|
||||
return True
|
||||
|
||||
|
||||
logger.info(f"Cleaning up {len(to_remove)} old backups")
|
||||
|
||||
|
||||
for backup in to_remove:
|
||||
try:
|
||||
backup["path"].unlink()
|
||||
logger.info(f"Removed backup: {backup['path'].name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not remove {backup['path'].name}: {e}")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to cleanup backups: {e}")
|
||||
return False
|
||||
@@ -503,7 +513,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {actual_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
# Validate global arguments
|
||||
success, errors = operation.validate_global_args(args)
|
||||
@@ -511,26 +521,27 @@ def run(args: argparse.Namespace) -> int:
|
||||
for error in errors:
|
||||
logger.error(error)
|
||||
return 1
|
||||
|
||||
|
||||
# Display header
|
||||
if not args.quiet:
|
||||
from setup.cli.base import __version__
|
||||
|
||||
display_header(
|
||||
f"SuperClaude Backup v{__version__}",
|
||||
"Backup and restore SuperClaude installations"
|
||||
"Backup and restore SuperClaude installations",
|
||||
)
|
||||
|
||||
|
||||
backup_dir = get_backup_directory(args)
|
||||
|
||||
|
||||
# Handle different backup operations
|
||||
if args.create:
|
||||
success = create_backup(args)
|
||||
|
||||
|
||||
elif args.list:
|
||||
backups = list_backups(backup_dir)
|
||||
display_backup_list(backups)
|
||||
success = True
|
||||
|
||||
|
||||
elif args.restore:
|
||||
if args.restore == "interactive":
|
||||
# Interactive restore
|
||||
@@ -544,14 +555,14 @@ def run(args: argparse.Namespace) -> int:
|
||||
backup_path = Path(args.restore)
|
||||
if not backup_path.is_absolute():
|
||||
backup_path = backup_dir / backup_path
|
||||
|
||||
|
||||
success = restore_backup(backup_path, args)
|
||||
|
||||
|
||||
elif args.info:
|
||||
backup_path = Path(args.info)
|
||||
if not backup_path.is_absolute():
|
||||
backup_path = backup_dir / backup_path
|
||||
|
||||
|
||||
info = get_backup_info(backup_path)
|
||||
if info["exists"]:
|
||||
print(f"\n{Colors.CYAN}Backup Information:{Colors.RESET}")
|
||||
@@ -559,10 +570,12 @@ def run(args: argparse.Namespace) -> int:
|
||||
print(f"Size: {format_size(info['size'])}")
|
||||
print(f"Created: {info['created']}")
|
||||
print(f"Files: {info.get('files', 'unknown')}")
|
||||
|
||||
|
||||
if info["metadata"]:
|
||||
metadata = info["metadata"]
|
||||
print(f"Framework Version: {metadata.get('framework_version', 'unknown')}")
|
||||
print(
|
||||
f"Framework Version: {metadata.get('framework_version', 'unknown')}"
|
||||
)
|
||||
if metadata.get("components"):
|
||||
print("Components:")
|
||||
for comp, ver in metadata["components"].items():
|
||||
@@ -571,14 +584,14 @@ def run(args: argparse.Namespace) -> int:
|
||||
logger.error(f"Backup file not found: {backup_path}")
|
||||
success = False
|
||||
success = True
|
||||
|
||||
|
||||
elif args.cleanup:
|
||||
success = cleanup_old_backups(backup_dir, args)
|
||||
|
||||
|
||||
else:
|
||||
logger.error("No backup operation specified")
|
||||
success = False
|
||||
|
||||
|
||||
if success:
|
||||
if not args.quiet and args.create:
|
||||
display_success("Backup operation completed successfully!")
|
||||
@@ -588,7 +601,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
else:
|
||||
display_error("Backup operation failed. Check logs for details.")
|
||||
return 1
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.YELLOW}Backup operation cancelled by user{Colors.RESET}")
|
||||
return 130
|
||||
|
||||
@@ -15,8 +15,17 @@ from ...core.registry import ComponentRegistry
|
||||
from ...services.config import ConfigService
|
||||
from ...core.validator import Validator
|
||||
from ...utils.ui import (
|
||||
display_header, display_info, display_success, display_error,
|
||||
display_warning, Menu, confirm, ProgressBar, Colors, format_size, prompt_api_key
|
||||
display_header,
|
||||
display_info,
|
||||
display_success,
|
||||
display_error,
|
||||
display_warning,
|
||||
Menu,
|
||||
confirm,
|
||||
ProgressBar,
|
||||
Colors,
|
||||
format_size,
|
||||
prompt_api_key,
|
||||
)
|
||||
from ...utils.environment import setup_environment_variables
|
||||
from ...utils.logger import get_logger
|
||||
@@ -26,7 +35,7 @@ from . import OperationBase
|
||||
|
||||
class InstallOperation(OperationBase):
|
||||
"""Installation operation implementation"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("install")
|
||||
|
||||
@@ -34,7 +43,7 @@ class InstallOperation(OperationBase):
|
||||
def register_parser(subparsers, global_parser=None) -> argparse.ArgumentParser:
|
||||
"""Register installation CLI arguments"""
|
||||
parents = [global_parser] if global_parser else []
|
||||
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"install",
|
||||
help="Install SuperClaude framework components",
|
||||
@@ -47,54 +56,51 @@ Examples:
|
||||
SuperClaude install --verbose --force # Verbose with force mode
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=parents
|
||||
parents=parents,
|
||||
)
|
||||
|
||||
|
||||
# Installation mode options
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"--components",
|
||||
type=str,
|
||||
nargs="+",
|
||||
help="Specific components to install"
|
||||
"--components", type=str, nargs="+", help="Specific components to install"
|
||||
)
|
||||
|
||||
|
||||
# Installation options
|
||||
parser.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Skip backup creation"
|
||||
)
|
||||
|
||||
parser.add_argument("--no-backup", action="store_true", help="Skip backup creation")
|
||||
|
||||
parser.add_argument(
|
||||
"--list-components",
|
||||
action="store_true",
|
||||
help="List available components and exit"
|
||||
help="List available components and exit",
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"--diagnose",
|
||||
action="store_true",
|
||||
help="Run system diagnostics and show installation help"
|
||||
help="Run system diagnostics and show installation help",
|
||||
)
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def validate_system_requirements(validator: Validator, component_names: List[str]) -> bool:
|
||||
def validate_system_requirements(
|
||||
validator: Validator, component_names: List[str]
|
||||
) -> bool:
|
||||
"""Validate system requirements"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
logger.info("Validating system requirements...")
|
||||
|
||||
|
||||
try:
|
||||
# Load requirements configuration
|
||||
config_manager = ConfigService(DATA_DIR)
|
||||
requirements = config_manager.get_requirements_for_components(component_names)
|
||||
|
||||
|
||||
# Validate requirements
|
||||
success, errors = validator.validate_component_requirements(component_names, requirements)
|
||||
|
||||
success, errors = validator.validate_component_requirements(
|
||||
component_names, requirements
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.success("All system requirements met")
|
||||
return True
|
||||
@@ -102,67 +108,79 @@ def validate_system_requirements(validator: Validator, component_names: List[str
|
||||
logger.error("System requirements not met:")
|
||||
for error in errors:
|
||||
logger.error(f" - {error}")
|
||||
|
||||
|
||||
# Provide additional guidance
|
||||
print(f"\n{Colors.CYAN}💡 Installation Help:{Colors.RESET}")
|
||||
print(" Run 'SuperClaude install --diagnose' for detailed system diagnostics")
|
||||
print(
|
||||
" Run 'superclaude install --diagnose' for detailed system diagnostics"
|
||||
)
|
||||
print(" and step-by-step installation instructions.")
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not validate system requirements: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_components_to_install(args: argparse.Namespace, registry: ComponentRegistry, config_manager: ConfigService) -> Optional[List[str]]:
|
||||
def get_components_to_install(
|
||||
args: argparse.Namespace, registry: ComponentRegistry, config_manager: ConfigService
|
||||
) -> Optional[List[str]]:
|
||||
"""Determine which components to install"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
# Explicit components specified
|
||||
if args.components:
|
||||
if 'all' in args.components:
|
||||
if "all" in args.components:
|
||||
components = ["core", "commands", "agents", "modes", "mcp", "mcp_docs"]
|
||||
else:
|
||||
components = args.components
|
||||
|
||||
# If mcp or mcp_docs is specified non-interactively, we should still ask which servers to install.
|
||||
if 'mcp' in components or 'mcp_docs' in components:
|
||||
if "mcp" in components or "mcp_docs" in components:
|
||||
selected_servers = select_mcp_servers(registry)
|
||||
if not hasattr(config_manager, '_installation_context'):
|
||||
if not hasattr(config_manager, "_installation_context"):
|
||||
config_manager._installation_context = {}
|
||||
config_manager._installation_context["selected_mcp_servers"] = selected_servers
|
||||
config_manager._installation_context["selected_mcp_servers"] = (
|
||||
selected_servers
|
||||
)
|
||||
|
||||
# If the user selected some servers, ensure both mcp and mcp_docs are included
|
||||
if selected_servers:
|
||||
if 'mcp' not in components:
|
||||
components.append('mcp')
|
||||
logger.debug(f"Auto-added 'mcp' component for selected servers: {selected_servers}")
|
||||
if 'mcp_docs' not in components:
|
||||
components.append('mcp_docs')
|
||||
logger.debug(f"Auto-added 'mcp_docs' component for selected servers: {selected_servers}")
|
||||
if "mcp" not in components:
|
||||
components.append("mcp")
|
||||
logger.debug(
|
||||
f"Auto-added 'mcp' component for selected servers: {selected_servers}"
|
||||
)
|
||||
if "mcp_docs" not in components:
|
||||
components.append("mcp_docs")
|
||||
logger.debug(
|
||||
f"Auto-added 'mcp_docs' component for selected servers: {selected_servers}"
|
||||
)
|
||||
|
||||
logger.info(f"Final components to install: {components}")
|
||||
|
||||
# If mcp_docs was explicitly requested but no servers selected, allow auto-detection
|
||||
elif not selected_servers and 'mcp_docs' in components:
|
||||
elif not selected_servers and "mcp_docs" in components:
|
||||
logger.info("mcp_docs component will auto-detect existing MCP servers")
|
||||
logger.info("Documentation will be installed for any detected servers")
|
||||
|
||||
return components
|
||||
|
||||
|
||||
# Interactive two-stage selection
|
||||
return interactive_component_selection(registry, config_manager)
|
||||
|
||||
|
||||
def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> Dict[str, str]:
|
||||
def collect_api_keys_for_servers(
|
||||
selected_servers: List[str], mcp_instance
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Collect API keys for servers that require them
|
||||
|
||||
|
||||
Args:
|
||||
selected_servers: List of selected server keys
|
||||
mcp_instance: MCP component instance
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of environment variable names to API key values
|
||||
"""
|
||||
@@ -170,132 +188,164 @@ def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> D
|
||||
servers_needing_keys = [
|
||||
(server_key, mcp_instance.mcp_servers[server_key])
|
||||
for server_key in selected_servers
|
||||
if server_key in mcp_instance.mcp_servers and
|
||||
mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
||||
if server_key in mcp_instance.mcp_servers
|
||||
and mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
||||
]
|
||||
|
||||
|
||||
if not servers_needing_keys:
|
||||
return {}
|
||||
|
||||
|
||||
# Display API key configuration header
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}=== API Key Configuration ==={Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n")
|
||||
|
||||
print(
|
||||
f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n"
|
||||
)
|
||||
|
||||
collected_keys = {}
|
||||
for server_key, server_info in servers_needing_keys:
|
||||
api_key_env = server_info.get("api_key_env")
|
||||
service_name = server_info["name"]
|
||||
|
||||
|
||||
if api_key_env:
|
||||
key = prompt_api_key(service_name, api_key_env)
|
||||
if key:
|
||||
collected_keys[api_key_env] = key
|
||||
|
||||
|
||||
return collected_keys
|
||||
|
||||
|
||||
def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
|
||||
"""Stage 1: MCP Server Selection with API Key Collection"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
# Get MCP component to access server list
|
||||
mcp_instance = registry.get_component_instance("mcp", get_home_directory() / ".claude")
|
||||
if not mcp_instance or not hasattr(mcp_instance, 'mcp_servers'):
|
||||
mcp_instance = registry.get_component_instance(
|
||||
"mcp", get_home_directory() / ".claude"
|
||||
)
|
||||
if not mcp_instance or not hasattr(mcp_instance, "mcp_servers"):
|
||||
logger.error("Could not access MCP server information")
|
||||
return []
|
||||
|
||||
|
||||
# Create MCP server menu
|
||||
mcp_servers = mcp_instance.mcp_servers
|
||||
server_options = []
|
||||
|
||||
|
||||
for server_key, server_info in mcp_servers.items():
|
||||
description = server_info["description"]
|
||||
api_key_note = " (requires API key)" if server_info.get("requires_api_key", False) else ""
|
||||
api_key_note = (
|
||||
" (requires API key)"
|
||||
if server_info.get("requires_api_key", False)
|
||||
else ""
|
||||
)
|
||||
server_options.append(f"{server_key} - {description}{api_key_note}")
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 1: MCP Server Selection (Optional){Colors.RESET}")
|
||||
print(
|
||||
f"{Colors.CYAN}{Colors.BRIGHT}Stage 1: MCP Server Selection (Optional){Colors.RESET}"
|
||||
)
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"\n{Colors.BLUE}MCP servers extend Claude Code with specialized capabilities.{Colors.RESET}")
|
||||
print(f"{Colors.BLUE}Select servers to configure (you can always add more later):{Colors.RESET}")
|
||||
|
||||
print(
|
||||
f"\n{Colors.BLUE}MCP servers extend Claude Code with specialized capabilities.{Colors.RESET}"
|
||||
)
|
||||
print(
|
||||
f"{Colors.BLUE}Select servers to configure (you can always add more later):{Colors.RESET}"
|
||||
)
|
||||
|
||||
# Add option to skip MCP
|
||||
server_options.append("Skip MCP Server installation")
|
||||
|
||||
menu = Menu("Select MCP servers to configure:", server_options, multi_select=True)
|
||||
|
||||
menu = Menu(
|
||||
"Select MCP servers to configure:", server_options, multi_select=True
|
||||
)
|
||||
selections = menu.display()
|
||||
|
||||
|
||||
if not selections:
|
||||
logger.info("No MCP servers selected")
|
||||
return []
|
||||
|
||||
|
||||
# Filter out the "skip" option and return server keys
|
||||
server_keys = list(mcp_servers.keys())
|
||||
selected_servers = []
|
||||
|
||||
|
||||
for i in selections:
|
||||
if i < len(server_keys): # Not the "skip" option
|
||||
selected_servers.append(server_keys[i])
|
||||
|
||||
|
||||
if selected_servers:
|
||||
logger.info(f"Selected MCP servers: {', '.join(selected_servers)}")
|
||||
|
||||
|
||||
# NEW: Collect API keys for selected servers
|
||||
collected_keys = collect_api_keys_for_servers(selected_servers, mcp_instance)
|
||||
|
||||
collected_keys = collect_api_keys_for_servers(
|
||||
selected_servers, mcp_instance
|
||||
)
|
||||
|
||||
# Set up environment variables
|
||||
if collected_keys:
|
||||
setup_environment_variables(collected_keys)
|
||||
|
||||
|
||||
# Store keys for MCP component to use during installation
|
||||
mcp_instance.collected_api_keys = collected_keys
|
||||
else:
|
||||
logger.info("No MCP servers selected")
|
||||
|
||||
|
||||
return selected_servers
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in MCP server selection: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def select_framework_components(registry: ComponentRegistry, config_manager: ConfigService, selected_mcp_servers: List[str]) -> List[str]:
|
||||
def select_framework_components(
|
||||
registry: ComponentRegistry,
|
||||
config_manager: ConfigService,
|
||||
selected_mcp_servers: List[str],
|
||||
) -> List[str]:
|
||||
"""Stage 2: Framework Component Selection"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
# Framework components (excluding MCP-related ones)
|
||||
framework_components = ["core", "modes", "commands", "agents"]
|
||||
|
||||
|
||||
# Create component menu
|
||||
component_options = []
|
||||
component_info = {}
|
||||
|
||||
|
||||
for component_name in framework_components:
|
||||
metadata = registry.get_component_metadata(component_name)
|
||||
if metadata:
|
||||
description = metadata.get("description", "No description")
|
||||
component_options.append(f"{component_name} - {description}")
|
||||
component_info[component_name] = metadata
|
||||
|
||||
|
||||
# Add MCP documentation option
|
||||
if selected_mcp_servers:
|
||||
mcp_docs_desc = f"MCP documentation for {', '.join(selected_mcp_servers)} (auto-selected)"
|
||||
component_options.append(f"mcp_docs - {mcp_docs_desc}")
|
||||
auto_selected_mcp_docs = True
|
||||
else:
|
||||
component_options.append("mcp_docs - MCP server documentation (none selected)")
|
||||
component_options.append(
|
||||
"mcp_docs - MCP server documentation (none selected)"
|
||||
)
|
||||
auto_selected_mcp_docs = False
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 2: Framework Component Selection{Colors.RESET}")
|
||||
print(
|
||||
f"{Colors.CYAN}{Colors.BRIGHT}Stage 2: Framework Component Selection{Colors.RESET}"
|
||||
)
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"\n{Colors.BLUE}Select SuperClaude framework components to install:{Colors.RESET}")
|
||||
|
||||
menu = Menu("Select components (Core is recommended):", component_options, multi_select=True)
|
||||
print(
|
||||
f"\n{Colors.BLUE}Select SuperClaude framework components to install:{Colors.RESET}"
|
||||
)
|
||||
|
||||
menu = Menu(
|
||||
"Select components (Core is recommended):",
|
||||
component_options,
|
||||
multi_select=True,
|
||||
)
|
||||
selections = menu.display()
|
||||
|
||||
|
||||
if not selections:
|
||||
# Default to core if nothing selected
|
||||
logger.info("No components selected, defaulting to core")
|
||||
@@ -303,11 +353,11 @@ def select_framework_components(registry: ComponentRegistry, config_manager: Con
|
||||
else:
|
||||
selected_components = []
|
||||
all_components = framework_components + ["mcp_docs"]
|
||||
|
||||
|
||||
for i in selections:
|
||||
if i < len(all_components):
|
||||
selected_components.append(all_components[i])
|
||||
|
||||
|
||||
# Auto-select MCP docs if not explicitly deselected and we have MCP servers
|
||||
if auto_selected_mcp_docs and "mcp_docs" not in selected_components:
|
||||
# Check if user explicitly deselected it
|
||||
@@ -316,82 +366,96 @@ def select_framework_components(registry: ComponentRegistry, config_manager: Con
|
||||
# User didn't select it, but we auto-select it
|
||||
selected_components.append("mcp_docs")
|
||||
logger.info("Auto-selected MCP documentation for configured servers")
|
||||
|
||||
|
||||
# Always include MCP component if servers were selected
|
||||
if selected_mcp_servers and "mcp" not in selected_components:
|
||||
selected_components.append("mcp")
|
||||
|
||||
|
||||
logger.info(f"Selected framework components: {', '.join(selected_components)}")
|
||||
return selected_components
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in framework component selection: {e}")
|
||||
return ["core"] # Fallback to core
|
||||
|
||||
|
||||
def interactive_component_selection(registry: ComponentRegistry, config_manager: ConfigService) -> Optional[List[str]]:
|
||||
def interactive_component_selection(
|
||||
registry: ComponentRegistry, config_manager: ConfigService
|
||||
) -> Optional[List[str]]:
|
||||
"""Two-stage interactive component selection"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
try:
|
||||
print(f"\n{Colors.CYAN}SuperClaude Interactive Installation{Colors.RESET}")
|
||||
print(f"{Colors.BLUE}Select components to install using the two-stage process:{Colors.RESET}")
|
||||
|
||||
print(
|
||||
f"{Colors.BLUE}Select components to install using the two-stage process:{Colors.RESET}"
|
||||
)
|
||||
|
||||
# Stage 1: MCP Server Selection
|
||||
selected_mcp_servers = select_mcp_servers(registry)
|
||||
|
||||
|
||||
# Stage 2: Framework Component Selection
|
||||
selected_components = select_framework_components(registry, config_manager, selected_mcp_servers)
|
||||
|
||||
selected_components = select_framework_components(
|
||||
registry, config_manager, selected_mcp_servers
|
||||
)
|
||||
|
||||
# Store selected MCP servers for components to use
|
||||
if not hasattr(config_manager, '_installation_context'):
|
||||
if not hasattr(config_manager, "_installation_context"):
|
||||
config_manager._installation_context = {}
|
||||
config_manager._installation_context["selected_mcp_servers"] = selected_mcp_servers
|
||||
|
||||
config_manager._installation_context["selected_mcp_servers"] = (
|
||||
selected_mcp_servers
|
||||
)
|
||||
|
||||
return selected_components
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in component selection: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def display_installation_plan(components: List[str], registry: ComponentRegistry, install_dir: Path) -> None:
|
||||
def display_installation_plan(
|
||||
components: List[str], registry: ComponentRegistry, install_dir: Path
|
||||
) -> None:
|
||||
"""Display installation plan"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}Installation Plan{Colors.RESET}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# Resolve dependencies
|
||||
try:
|
||||
ordered_components = registry.resolve_dependencies(components)
|
||||
|
||||
|
||||
print(f"{Colors.BLUE}Installation Directory:{Colors.RESET} {install_dir}")
|
||||
print(f"{Colors.BLUE}Components to install:{Colors.RESET}")
|
||||
|
||||
|
||||
total_size = 0
|
||||
for i, component_name in enumerate(ordered_components, 1):
|
||||
metadata = registry.get_component_metadata(component_name)
|
||||
if metadata:
|
||||
description = metadata.get("description", "No description")
|
||||
print(f" {i}. {component_name} - {description}")
|
||||
|
||||
|
||||
# Get size estimate if component supports it
|
||||
try:
|
||||
instance = registry.get_component_instance(component_name, install_dir)
|
||||
if instance and hasattr(instance, 'get_size_estimate'):
|
||||
instance = registry.get_component_instance(
|
||||
component_name, install_dir
|
||||
)
|
||||
if instance and hasattr(instance, "get_size_estimate"):
|
||||
size = instance.get_size_estimate()
|
||||
total_size += size
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print(f" {i}. {component_name} - Unknown component")
|
||||
|
||||
|
||||
if total_size > 0:
|
||||
print(f"\n{Colors.BLUE}Estimated size:{Colors.RESET} {format_size(total_size)}")
|
||||
|
||||
print(
|
||||
f"\n{Colors.BLUE}Estimated size:{Colors.RESET} {format_size(total_size)}"
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not resolve dependencies: {e}")
|
||||
raise
|
||||
@@ -400,101 +464,113 @@ def display_installation_plan(components: List[str], registry: ComponentRegistry
|
||||
def run_system_diagnostics(validator: Validator) -> None:
|
||||
"""Run comprehensive system diagnostics"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}SuperClaude System Diagnostics{Colors.RESET}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# Run diagnostics
|
||||
diagnostics = validator.diagnose_system()
|
||||
|
||||
|
||||
# Display platform info
|
||||
print(f"{Colors.BLUE}Platform:{Colors.RESET} {diagnostics['platform']}")
|
||||
|
||||
|
||||
# Display check results
|
||||
print(f"\n{Colors.BLUE}System Checks:{Colors.RESET}")
|
||||
all_passed = True
|
||||
|
||||
for check_name, check_info in diagnostics['checks'].items():
|
||||
status = check_info['status']
|
||||
message = check_info['message']
|
||||
|
||||
if status == 'pass':
|
||||
|
||||
for check_name, check_info in diagnostics["checks"].items():
|
||||
status = check_info["status"]
|
||||
message = check_info["message"]
|
||||
|
||||
if status == "pass":
|
||||
print(f" ✅ {check_name}: {message}")
|
||||
else:
|
||||
print(f" ❌ {check_name}: {message}")
|
||||
all_passed = False
|
||||
|
||||
|
||||
# Display issues and recommendations
|
||||
if diagnostics['issues']:
|
||||
if diagnostics["issues"]:
|
||||
print(f"\n{Colors.YELLOW}Issues Found:{Colors.RESET}")
|
||||
for issue in diagnostics['issues']:
|
||||
for issue in diagnostics["issues"]:
|
||||
print(f" ⚠️ {issue}")
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}Recommendations:{Colors.RESET}")
|
||||
for recommendation in diagnostics['recommendations']:
|
||||
for recommendation in diagnostics["recommendations"]:
|
||||
print(recommendation)
|
||||
|
||||
|
||||
# Summary
|
||||
if all_passed:
|
||||
print(f"\n{Colors.GREEN}✅ All system checks passed! Your system is ready for SuperClaude.{Colors.RESET}")
|
||||
print(
|
||||
f"\n{Colors.GREEN}✅ All system checks passed! Your system is ready for superclaude.{Colors.RESET}"
|
||||
)
|
||||
else:
|
||||
print(f"\n{Colors.YELLOW}⚠️ Some issues found. Please address the recommendations above.{Colors.RESET}")
|
||||
|
||||
print(
|
||||
f"\n{Colors.YELLOW}⚠️ Some issues found. Please address the recommendations above.{Colors.RESET}"
|
||||
)
|
||||
|
||||
print(f"\n{Colors.BLUE}Next steps:{Colors.RESET}")
|
||||
if all_passed:
|
||||
print(" 1. Run 'SuperClaude install' to proceed with installation")
|
||||
print(" 2. Choose your preferred installation mode (quick, minimal, or custom)")
|
||||
print(" 1. Run 'superclaude install' to proceed with installation")
|
||||
print(
|
||||
" 2. Choose your preferred installation mode (quick, minimal, or custom)"
|
||||
)
|
||||
else:
|
||||
print(" 1. Install missing dependencies using the commands above")
|
||||
print(" 2. Restart your terminal after installing tools")
|
||||
print(" 3. Run 'SuperClaude install --diagnose' again to verify")
|
||||
print(" 3. Run 'superclaude install --diagnose' again to verify")
|
||||
|
||||
|
||||
def perform_installation(components: List[str], args: argparse.Namespace, config_manager: ConfigService = None) -> bool:
|
||||
def perform_installation(
|
||||
components: List[str],
|
||||
args: argparse.Namespace,
|
||||
config_manager: ConfigService = None,
|
||||
) -> bool:
|
||||
"""Perform the actual installation"""
|
||||
logger = get_logger()
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
try:
|
||||
# Create installer
|
||||
installer = Installer(args.install_dir, dry_run=args.dry_run)
|
||||
|
||||
|
||||
# Create component registry
|
||||
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
||||
registry.discover_components()
|
||||
|
||||
|
||||
# Create component instances
|
||||
component_instances = registry.create_component_instances(components, args.install_dir)
|
||||
|
||||
component_instances = registry.create_component_instances(
|
||||
components, args.install_dir
|
||||
)
|
||||
|
||||
if not component_instances:
|
||||
logger.error("No valid component instances created")
|
||||
return False
|
||||
|
||||
|
||||
# Register components with installer
|
||||
installer.register_components(list(component_instances.values()))
|
||||
|
||||
|
||||
# The 'components' list is already resolved, so we can use it directly.
|
||||
ordered_components = components
|
||||
|
||||
|
||||
# Setup progress tracking
|
||||
progress = ProgressBar(
|
||||
total=len(ordered_components),
|
||||
prefix="Installing: ",
|
||||
suffix=""
|
||||
total=len(ordered_components), prefix="Installing: ", suffix=""
|
||||
)
|
||||
|
||||
|
||||
# Install components
|
||||
logger.info(f"Installing {len(ordered_components)} components...")
|
||||
|
||||
|
||||
config = {
|
||||
"force": args.force,
|
||||
"backup": not args.no_backup,
|
||||
"dry_run": args.dry_run,
|
||||
"selected_mcp_servers": getattr(config_manager, '_installation_context', {}).get("selected_mcp_servers", [])
|
||||
"selected_mcp_servers": getattr(
|
||||
config_manager, "_installation_context", {}
|
||||
).get("selected_mcp_servers", []),
|
||||
}
|
||||
|
||||
|
||||
success = installer.install_components(ordered_components, config)
|
||||
|
||||
|
||||
# Update progress
|
||||
for i, component_name in enumerate(ordered_components):
|
||||
if component_name in installer.installed_components:
|
||||
@@ -502,32 +578,36 @@ def perform_installation(components: List[str], args: argparse.Namespace, config
|
||||
else:
|
||||
progress.update(i + 1, f"Failed {component_name}")
|
||||
time.sleep(0.1) # Brief pause for visual effect
|
||||
|
||||
|
||||
progress.finish("Installation complete")
|
||||
|
||||
|
||||
# Show results
|
||||
duration = time.time() - start_time
|
||||
|
||||
|
||||
if success:
|
||||
logger.success(f"Installation completed successfully in {duration:.1f} seconds")
|
||||
|
||||
logger.success(
|
||||
f"Installation completed successfully in {duration:.1f} seconds"
|
||||
)
|
||||
|
||||
# Show summary
|
||||
summary = installer.get_installation_summary()
|
||||
if summary['installed']:
|
||||
if summary["installed"]:
|
||||
logger.info(f"Installed components: {', '.join(summary['installed'])}")
|
||||
|
||||
if summary['backup_path']:
|
||||
|
||||
if summary["backup_path"]:
|
||||
logger.info(f"Backup created: {summary['backup_path']}")
|
||||
|
||||
|
||||
else:
|
||||
logger.error(f"Installation completed with errors in {duration:.1f} seconds")
|
||||
|
||||
logger.error(
|
||||
f"Installation completed with errors in {duration:.1f} seconds"
|
||||
)
|
||||
|
||||
summary = installer.get_installation_summary()
|
||||
if summary['failed']:
|
||||
if summary["failed"]:
|
||||
logger.error(f"Failed components: {', '.join(summary['failed'])}")
|
||||
|
||||
|
||||
return success
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error during installation: {e}")
|
||||
return False
|
||||
@@ -547,18 +627,18 @@ def run(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
# Verify the resolved path is still within user home
|
||||
install_dir_resolved.relative_to(expected_home)
|
||||
|
||||
|
||||
# Additional check: if there's a symlink in the path, verify it doesn't escape user home
|
||||
if install_dir_original != install_dir_resolved:
|
||||
# Path contains symlinks - verify each component stays within user home
|
||||
current_path = expected_home
|
||||
parts = install_dir_original.parts
|
||||
home_parts = expected_home.parts
|
||||
|
||||
|
||||
# Skip home directory parts
|
||||
if len(parts) >= len(home_parts) and parts[:len(home_parts)] == home_parts:
|
||||
relative_parts = parts[len(home_parts):]
|
||||
|
||||
if len(parts) >= len(home_parts) and parts[: len(home_parts)] == home_parts:
|
||||
relative_parts = parts[len(home_parts) :]
|
||||
|
||||
for part in relative_parts:
|
||||
current_path = current_path / part
|
||||
if current_path.is_symlink():
|
||||
@@ -575,7 +655,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
print(f"\n[x] Security validation failed: {e}")
|
||||
print(f" Please use a standard directory path within your user profile.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
# Validate global arguments
|
||||
success, errors = operation.validate_global_args(args)
|
||||
@@ -583,20 +663,21 @@ def run(args: argparse.Namespace) -> int:
|
||||
for error in errors:
|
||||
logger.error(error)
|
||||
return 1
|
||||
|
||||
|
||||
# Display header
|
||||
if not args.quiet:
|
||||
from setup.cli.base import __version__
|
||||
|
||||
display_header(
|
||||
f"SuperClaude Installation v{__version__}",
|
||||
"Installing SuperClaude framework components"
|
||||
"Installing SuperClaude framework components",
|
||||
)
|
||||
|
||||
|
||||
# Handle special modes
|
||||
if args.list_components:
|
||||
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
||||
registry.discover_components()
|
||||
|
||||
|
||||
components = registry.list_components()
|
||||
if components:
|
||||
print(f"\n{Colors.CYAN}Available Components:{Colors.RESET}")
|
||||
@@ -611,22 +692,22 @@ def run(args: argparse.Namespace) -> int:
|
||||
else:
|
||||
print("No components found")
|
||||
return 0
|
||||
|
||||
|
||||
# Handle diagnostic mode
|
||||
if args.diagnose:
|
||||
validator = Validator()
|
||||
run_system_diagnostics(validator)
|
||||
return 0
|
||||
|
||||
|
||||
# Create component registry and load configuration
|
||||
logger.info("Initializing installation system...")
|
||||
|
||||
|
||||
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
||||
registry.discover_components()
|
||||
|
||||
|
||||
config_manager = ConfigService(DATA_DIR)
|
||||
validator = Validator()
|
||||
|
||||
|
||||
# Validate configuration
|
||||
config_errors = config_manager.validate_config_files()
|
||||
if config_errors:
|
||||
@@ -634,9 +715,11 @@ def run(args: argparse.Namespace) -> int:
|
||||
for error in config_errors:
|
||||
logger.error(f" - {error}")
|
||||
return 1
|
||||
|
||||
|
||||
# Get components to install
|
||||
components_to_install = get_components_to_install(args, registry, config_manager)
|
||||
components_to_install = get_components_to_install(
|
||||
args, registry, config_manager
|
||||
)
|
||||
if not components_to_install:
|
||||
logger.error("No components selected for installation")
|
||||
return 1
|
||||
@@ -647,50 +730,58 @@ def run(args: argparse.Namespace) -> int:
|
||||
except ValueError as e:
|
||||
logger.error(f"Dependency resolution error: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
# Validate system requirements for all components
|
||||
if not validate_system_requirements(validator, resolved_components):
|
||||
if not args.force:
|
||||
logger.error("System requirements not met. Use --force to override.")
|
||||
return 1
|
||||
else:
|
||||
logger.warning("System requirements not met, but continuing due to --force flag")
|
||||
|
||||
logger.warning(
|
||||
"System requirements not met, but continuing due to --force flag"
|
||||
)
|
||||
|
||||
# Check for existing installation
|
||||
if args.install_dir.exists() and not args.force:
|
||||
if not args.dry_run:
|
||||
logger.warning(f"Installation directory already exists: {args.install_dir}")
|
||||
if not args.yes and not confirm("Continue and update existing installation?", default=False):
|
||||
logger.warning(
|
||||
f"Installation directory already exists: {args.install_dir}"
|
||||
)
|
||||
if not args.yes and not confirm(
|
||||
"Continue and update existing installation?", default=False
|
||||
):
|
||||
logger.info("Installation cancelled by user")
|
||||
return 0
|
||||
|
||||
|
||||
# Display installation plan
|
||||
if not args.quiet:
|
||||
display_installation_plan(resolved_components, registry, args.install_dir)
|
||||
|
||||
|
||||
if not args.dry_run:
|
||||
if not args.yes and not confirm("Proceed with installation?", default=True):
|
||||
if not args.yes and not confirm(
|
||||
"Proceed with installation?", default=True
|
||||
):
|
||||
logger.info("Installation cancelled by user")
|
||||
return 0
|
||||
|
||||
|
||||
# Perform installation
|
||||
success = perform_installation(resolved_components, args, config_manager)
|
||||
|
||||
|
||||
if success:
|
||||
if not args.quiet:
|
||||
display_success("SuperClaude installation completed successfully!")
|
||||
|
||||
|
||||
if not args.dry_run:
|
||||
print(f"\n{Colors.CYAN}Next steps:{Colors.RESET}")
|
||||
print(f"1. Restart your Claude Code session")
|
||||
print(f"2. Framework files are now available in {args.install_dir}")
|
||||
print(f"3. Use SuperClaude commands and features in Claude Code")
|
||||
|
||||
|
||||
return 0
|
||||
else:
|
||||
display_error("Installation failed. Check logs for details.")
|
||||
return 1
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.YELLOW}Installation cancelled by user{Colors.RESET}")
|
||||
return 130
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,17 @@ from ...core.registry import ComponentRegistry
|
||||
from ...services.settings import SettingsService
|
||||
from ...core.validator import Validator
|
||||
from ...utils.ui import (
|
||||
display_header, display_info, display_success, display_error,
|
||||
display_warning, Menu, confirm, ProgressBar, Colors, format_size, prompt_api_key
|
||||
display_header,
|
||||
display_info,
|
||||
display_success,
|
||||
display_error,
|
||||
display_warning,
|
||||
Menu,
|
||||
confirm,
|
||||
ProgressBar,
|
||||
Colors,
|
||||
format_size,
|
||||
prompt_api_key,
|
||||
)
|
||||
from ...utils.environment import setup_environment_variables
|
||||
from ...utils.logger import get_logger
|
||||
@@ -26,7 +35,7 @@ from . import OperationBase
|
||||
|
||||
class UpdateOperation(OperationBase):
|
||||
"""Update operation implementation"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("update")
|
||||
|
||||
@@ -34,7 +43,7 @@ class UpdateOperation(OperationBase):
|
||||
def register_parser(subparsers, global_parser=None) -> argparse.ArgumentParser:
|
||||
"""Register update CLI arguments"""
|
||||
parents = [global_parser] if global_parser else []
|
||||
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"update",
|
||||
help="Update existing SuperClaude installation",
|
||||
@@ -47,51 +56,44 @@ Examples:
|
||||
SuperClaude update --backup --force # Create backup before update (forced)
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=parents
|
||||
parents=parents,
|
||||
)
|
||||
|
||||
|
||||
# Update mode options
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Check for available updates without installing"
|
||||
help="Check for available updates without installing",
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"--components",
|
||||
type=str,
|
||||
nargs="+",
|
||||
help="Specific components to update"
|
||||
"--components", type=str, nargs="+", help="Specific components to update"
|
||||
)
|
||||
|
||||
|
||||
# Backup options
|
||||
parser.add_argument(
|
||||
"--backup",
|
||||
action="store_true",
|
||||
help="Create backup before update"
|
||||
"--backup", action="store_true", help="Create backup before update"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Skip backup creation"
|
||||
)
|
||||
|
||||
|
||||
parser.add_argument("--no-backup", action="store_true", help="Skip backup creation")
|
||||
|
||||
# Update options
|
||||
parser.add_argument(
|
||||
"--reinstall",
|
||||
action="store_true",
|
||||
help="Reinstall components even if versions match"
|
||||
help="Reinstall components even if versions match",
|
||||
)
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def check_installation_exists(install_dir: Path) -> bool:
|
||||
"""Check if SuperClaude installation exists"""
|
||||
settings_manager = SettingsService(install_dir)
|
||||
|
||||
return settings_manager.check_installation_exists()
|
||||
|
||||
|
||||
def get_installed_components(install_dir: Path) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get currently installed components and their versions"""
|
||||
try:
|
||||
@@ -101,10 +103,12 @@ def get_installed_components(install_dir: Path) -> Dict[str, Dict[str, Any]]:
|
||||
return {}
|
||||
|
||||
|
||||
def get_available_updates(installed_components: Dict[str, str], registry: ComponentRegistry) -> Dict[str, Dict[str, str]]:
|
||||
def get_available_updates(
|
||||
installed_components: Dict[str, str], registry: ComponentRegistry
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
"""Check for available updates"""
|
||||
updates = {}
|
||||
|
||||
|
||||
for component_name, current_version in installed_components.items():
|
||||
try:
|
||||
metadata = registry.get_component_metadata(component_name)
|
||||
@@ -114,27 +118,29 @@ def get_available_updates(installed_components: Dict[str, str], registry: Compon
|
||||
updates[component_name] = {
|
||||
"current": current_version,
|
||||
"available": available_version,
|
||||
"description": metadata.get("description", "No description")
|
||||
"description": metadata.get("description", "No description"),
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
return updates
|
||||
|
||||
|
||||
def display_update_check(installed_components: Dict[str, str], available_updates: Dict[str, Dict[str, str]]) -> None:
|
||||
def display_update_check(
|
||||
installed_components: Dict[str, str], available_updates: Dict[str, Dict[str, str]]
|
||||
) -> None:
|
||||
"""Display update check results"""
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}Update Check Results{Colors.RESET}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if not installed_components:
|
||||
print(f"{Colors.YELLOW}No SuperClaude installation found{Colors.RESET}")
|
||||
return
|
||||
|
||||
|
||||
print(f"{Colors.BLUE}Currently installed components:{Colors.RESET}")
|
||||
for component, version in installed_components.items():
|
||||
print(f" {component}: v{version}")
|
||||
|
||||
|
||||
if available_updates:
|
||||
print(f"\n{Colors.GREEN}Available updates:{Colors.RESET}")
|
||||
for component, info in available_updates.items():
|
||||
@@ -142,47 +148,54 @@ def display_update_check(installed_components: Dict[str, str], available_updates
|
||||
print(f" {info['description']}")
|
||||
else:
|
||||
print(f"\n{Colors.GREEN}All components are up to date{Colors.RESET}")
|
||||
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def get_components_to_update(args: argparse.Namespace, installed_components: Dict[str, str],
|
||||
available_updates: Dict[str, Dict[str, str]]) -> Optional[List[str]]:
|
||||
def get_components_to_update(
|
||||
args: argparse.Namespace,
|
||||
installed_components: Dict[str, str],
|
||||
available_updates: Dict[str, Dict[str, str]],
|
||||
) -> Optional[List[str]]:
|
||||
"""Determine which components to update"""
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
# Explicit components specified
|
||||
if args.components:
|
||||
# Validate that specified components are installed
|
||||
invalid_components = [c for c in args.components if c not in installed_components]
|
||||
invalid_components = [
|
||||
c for c in args.components if c not in installed_components
|
||||
]
|
||||
if invalid_components:
|
||||
logger.error(f"Components not installed: {invalid_components}")
|
||||
return None
|
||||
return args.components
|
||||
|
||||
|
||||
# If no updates available and not forcing reinstall
|
||||
if not available_updates and not args.reinstall:
|
||||
logger.info("No updates available")
|
||||
return []
|
||||
|
||||
|
||||
# Interactive selection
|
||||
if available_updates:
|
||||
return interactive_update_selection(available_updates, installed_components)
|
||||
elif args.reinstall:
|
||||
# Reinstall all components
|
||||
return list(installed_components.keys())
|
||||
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> Dict[str, str]:
|
||||
def collect_api_keys_for_servers(
|
||||
selected_servers: List[str], mcp_instance
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Collect API keys for servers that require them during update
|
||||
|
||||
|
||||
Args:
|
||||
selected_servers: List of selected server keys
|
||||
mcp_instance: MCP component instance
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of environment variable names to API key values
|
||||
"""
|
||||
@@ -190,81 +203,90 @@ def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> D
|
||||
servers_needing_keys = [
|
||||
(server_key, mcp_instance.mcp_servers[server_key])
|
||||
for server_key in selected_servers
|
||||
if server_key in mcp_instance.mcp_servers and
|
||||
mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
||||
if server_key in mcp_instance.mcp_servers
|
||||
and mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
||||
]
|
||||
|
||||
|
||||
if not servers_needing_keys:
|
||||
return {}
|
||||
|
||||
|
||||
# Display API key configuration header
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}=== API Key Configuration ==={Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}New MCP servers require API keys for full functionality:{Colors.RESET}\n")
|
||||
|
||||
print(
|
||||
f"{Colors.YELLOW}New MCP servers require API keys for full functionality:{Colors.RESET}\n"
|
||||
)
|
||||
|
||||
collected_keys = {}
|
||||
for server_key, server_info in servers_needing_keys:
|
||||
api_key_env = server_info.get("api_key_env")
|
||||
service_name = server_info["name"]
|
||||
|
||||
|
||||
if api_key_env:
|
||||
key = prompt_api_key(service_name, api_key_env)
|
||||
if key:
|
||||
collected_keys[api_key_env] = key
|
||||
|
||||
|
||||
return collected_keys
|
||||
|
||||
|
||||
def interactive_update_selection(available_updates: Dict[str, Dict[str, str]],
|
||||
installed_components: Dict[str, str]) -> Optional[List[str]]:
|
||||
def interactive_update_selection(
|
||||
available_updates: Dict[str, Dict[str, str]], installed_components: Dict[str, str]
|
||||
) -> Optional[List[str]]:
|
||||
"""Interactive update selection"""
|
||||
if not available_updates:
|
||||
return []
|
||||
|
||||
|
||||
print(f"\n{Colors.CYAN}Available Updates:{Colors.RESET}")
|
||||
|
||||
|
||||
# Create menu options
|
||||
update_options = []
|
||||
component_names = []
|
||||
|
||||
|
||||
for component, info in available_updates.items():
|
||||
update_options.append(f"{component}: v{info['current']} → v{info['available']}")
|
||||
component_names.append(component)
|
||||
|
||||
|
||||
# Add bulk options
|
||||
preset_options = [
|
||||
"Update All Components",
|
||||
"Select Individual Components",
|
||||
"Cancel Update"
|
||||
"Select Individual Components",
|
||||
"Cancel Update",
|
||||
]
|
||||
|
||||
|
||||
menu = Menu("Select update option:", preset_options)
|
||||
choice = menu.display()
|
||||
|
||||
|
||||
if choice == -1 or choice == 2: # Cancelled
|
||||
return None
|
||||
elif choice == 0: # Update all
|
||||
return component_names
|
||||
elif choice == 1: # Select individual
|
||||
component_menu = Menu("Select components to update:", update_options, multi_select=True)
|
||||
component_menu = Menu(
|
||||
"Select components to update:", update_options, multi_select=True
|
||||
)
|
||||
selections = component_menu.display()
|
||||
|
||||
|
||||
if not selections:
|
||||
return None
|
||||
|
||||
|
||||
return [component_names[i] for i in selections]
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def display_update_plan(components: List[str], available_updates: Dict[str, Dict[str, str]],
|
||||
installed_components: Dict[str, str], install_dir: Path) -> None:
|
||||
def display_update_plan(
|
||||
components: List[str],
|
||||
available_updates: Dict[str, Dict[str, str]],
|
||||
installed_components: Dict[str, str],
|
||||
install_dir: Path,
|
||||
) -> None:
|
||||
"""Display update plan"""
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}Update Plan{Colors.RESET}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
print(f"{Colors.BLUE}Installation Directory:{Colors.RESET} {install_dir}")
|
||||
print(f"{Colors.BLUE}Components to update:{Colors.RESET}")
|
||||
|
||||
|
||||
for i, component_name in enumerate(components, 1):
|
||||
if component_name in available_updates:
|
||||
info = available_updates[component_name]
|
||||
@@ -272,72 +294,80 @@ def display_update_plan(components: List[str], available_updates: Dict[str, Dict
|
||||
else:
|
||||
current_version = installed_components.get(component_name, "unknown")
|
||||
print(f" {i}. {component_name}: v{current_version} (reinstall)")
|
||||
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def perform_update(components: List[str], args: argparse.Namespace, registry: ComponentRegistry) -> bool:
|
||||
def perform_update(
|
||||
components: List[str], args: argparse.Namespace, registry: ComponentRegistry
|
||||
) -> bool:
|
||||
"""Perform the actual update"""
|
||||
logger = get_logger()
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
try:
|
||||
# Create installer
|
||||
installer = Installer(args.install_dir, dry_run=args.dry_run)
|
||||
|
||||
|
||||
# Create component instances
|
||||
component_instances = registry.create_component_instances(components, args.install_dir)
|
||||
|
||||
component_instances = registry.create_component_instances(
|
||||
components, args.install_dir
|
||||
)
|
||||
|
||||
if not component_instances:
|
||||
logger.error("No valid component instances created")
|
||||
return False
|
||||
|
||||
|
||||
# Handle MCP component specially - collect API keys for new servers
|
||||
collected_api_keys = {}
|
||||
if "mcp" in components and "mcp" in component_instances:
|
||||
mcp_instance = component_instances["mcp"]
|
||||
if hasattr(mcp_instance, 'mcp_servers'):
|
||||
if hasattr(mcp_instance, "mcp_servers"):
|
||||
# Get all available MCP servers
|
||||
all_server_keys = list(mcp_instance.mcp_servers.keys())
|
||||
|
||||
|
||||
# Collect API keys for any servers that require them
|
||||
collected_api_keys = collect_api_keys_for_servers(all_server_keys, mcp_instance)
|
||||
|
||||
collected_api_keys = collect_api_keys_for_servers(
|
||||
all_server_keys, mcp_instance
|
||||
)
|
||||
|
||||
# Set up environment variables if any keys were collected
|
||||
if collected_api_keys:
|
||||
setup_environment_variables(collected_api_keys)
|
||||
|
||||
|
||||
# Store keys for MCP component to use during update
|
||||
mcp_instance.collected_api_keys = collected_api_keys
|
||||
|
||||
logger.info(f"Collected {len(collected_api_keys)} API keys for MCP server update")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Collected {len(collected_api_keys)} API keys for MCP server update"
|
||||
)
|
||||
|
||||
# Register components with installer
|
||||
installer.register_components(list(component_instances.values()))
|
||||
|
||||
|
||||
# Setup progress tracking
|
||||
progress = ProgressBar(
|
||||
total=len(components),
|
||||
prefix="Updating: ",
|
||||
suffix=""
|
||||
)
|
||||
|
||||
progress = ProgressBar(total=len(components), prefix="Updating: ", suffix="")
|
||||
|
||||
# Update components
|
||||
logger.info(f"Updating {len(components)} components...")
|
||||
|
||||
|
||||
# Determine backup strategy
|
||||
backup = args.backup or (not args.no_backup and not args.dry_run)
|
||||
|
||||
|
||||
config = {
|
||||
"force": args.force,
|
||||
"backup": backup,
|
||||
"dry_run": args.dry_run,
|
||||
"update_mode": True,
|
||||
"selected_mcp_servers": list(mcp_instance.mcp_servers.keys()) if "mcp" in component_instances else []
|
||||
"selected_mcp_servers": (
|
||||
list(mcp_instance.mcp_servers.keys())
|
||||
if "mcp" in component_instances
|
||||
else []
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
success = installer.update_components(components, config)
|
||||
|
||||
|
||||
# Update progress
|
||||
for i, component_name in enumerate(components):
|
||||
if component_name in installer.updated_components:
|
||||
@@ -345,32 +375,32 @@ def perform_update(components: List[str], args: argparse.Namespace, registry: Co
|
||||
else:
|
||||
progress.update(i + 1, f"Failed {component_name}")
|
||||
time.sleep(0.1) # Brief pause for visual effect
|
||||
|
||||
|
||||
progress.finish("Update complete")
|
||||
|
||||
|
||||
# Show results
|
||||
duration = time.time() - start_time
|
||||
|
||||
|
||||
if success:
|
||||
logger.success(f"Update completed successfully in {duration:.1f} seconds")
|
||||
|
||||
|
||||
# Show summary
|
||||
summary = installer.get_update_summary()
|
||||
if summary.get('updated'):
|
||||
if summary.get("updated"):
|
||||
logger.info(f"Updated components: {', '.join(summary['updated'])}")
|
||||
|
||||
if summary.get('backup_path'):
|
||||
|
||||
if summary.get("backup_path"):
|
||||
logger.info(f"Backup created: {summary['backup_path']}")
|
||||
|
||||
|
||||
else:
|
||||
logger.error(f"Update completed with errors in {duration:.1f} seconds")
|
||||
|
||||
|
||||
summary = installer.get_update_summary()
|
||||
if summary.get('failed'):
|
||||
if summary.get("failed"):
|
||||
logger.error(f"Failed components: {', '.join(summary['failed'])}")
|
||||
|
||||
|
||||
return success
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error during update: {e}")
|
||||
return False
|
||||
@@ -393,7 +423,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {actual_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
# Validate global arguments
|
||||
success, errors = operation.validate_global_args(args)
|
||||
@@ -401,79 +431,83 @@ def run(args: argparse.Namespace) -> int:
|
||||
for error in errors:
|
||||
logger.error(error)
|
||||
return 1
|
||||
|
||||
|
||||
# Display header
|
||||
if not args.quiet:
|
||||
display_header(
|
||||
f"SuperClaude Update v{__version__}",
|
||||
"Updating SuperClaude framework components"
|
||||
"Updating SuperClaude framework components",
|
||||
)
|
||||
|
||||
|
||||
# Check if SuperClaude is installed
|
||||
if not check_installation_exists(args.install_dir):
|
||||
logger.error(f"SuperClaude installation not found in {args.install_dir}")
|
||||
logger.info("Use 'SuperClaude install' to install SuperClaude first")
|
||||
logger.info("Use 'superclaude install' to install SuperClaude first")
|
||||
return 1
|
||||
|
||||
|
||||
# Create component registry
|
||||
logger.info("Checking for available updates...")
|
||||
|
||||
|
||||
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
||||
registry.discover_components()
|
||||
|
||||
|
||||
# Get installed components
|
||||
installed_components = get_installed_components(args.install_dir)
|
||||
if not installed_components:
|
||||
logger.error("Could not determine installed components")
|
||||
return 1
|
||||
|
||||
|
||||
# Check for available updates
|
||||
available_updates = get_available_updates(installed_components, registry)
|
||||
|
||||
|
||||
# Display update check results
|
||||
if not args.quiet:
|
||||
display_update_check(installed_components, available_updates)
|
||||
|
||||
|
||||
# If only checking for updates, exit here
|
||||
if args.check:
|
||||
return 0
|
||||
|
||||
|
||||
# Get components to update
|
||||
components = get_components_to_update(args, installed_components, available_updates)
|
||||
components = get_components_to_update(
|
||||
args, installed_components, available_updates
|
||||
)
|
||||
if components is None:
|
||||
logger.info("Update cancelled by user")
|
||||
return 0
|
||||
elif not components:
|
||||
logger.info("No components selected for update")
|
||||
return 0
|
||||
|
||||
|
||||
# Display update plan
|
||||
if not args.quiet:
|
||||
display_update_plan(components, available_updates, installed_components, args.install_dir)
|
||||
|
||||
display_update_plan(
|
||||
components, available_updates, installed_components, args.install_dir
|
||||
)
|
||||
|
||||
if not args.dry_run:
|
||||
if not args.yes and not confirm("Proceed with update?", default=True):
|
||||
logger.info("Update cancelled by user")
|
||||
return 0
|
||||
|
||||
|
||||
# Perform update
|
||||
success = perform_update(components, args, registry)
|
||||
|
||||
|
||||
if success:
|
||||
if not args.quiet:
|
||||
display_success("SuperClaude update completed successfully!")
|
||||
|
||||
|
||||
if not args.dry_run:
|
||||
print(f"\n{Colors.CYAN}Next steps:{Colors.RESET}")
|
||||
print(f"1. Restart your Claude Code session")
|
||||
print(f"2. Updated components are now available")
|
||||
print(f"3. Check for any breaking changes in documentation")
|
||||
|
||||
|
||||
return 0
|
||||
else:
|
||||
display_error("Update failed. Check logs for details.")
|
||||
return 1
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.YELLOW}Update cancelled by user{Colors.RESET}")
|
||||
return 130
|
||||
|
||||
Reference in New Issue
Block a user