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:
kazuki nakai
2025-10-14 12:17:09 +09:00
committed by GitHub
parent 302c5851b1
commit 050d5ea2ab
194 changed files with 9698 additions and 3693 deletions

View File

@@ -7,5 +7,5 @@ from .base import OperationBase
from .commands import *
__all__ = [
'OperationBase',
]
"OperationBase",
]

View File

@@ -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

View File

@@ -10,9 +10,9 @@ from .update import UpdateOperation
from .backup import BackupOperation
__all__ = [
'OperationBase',
'InstallOperation',
'UninstallOperation',
'UpdateOperation',
'BackupOperation'
]
"OperationBase",
"InstallOperation",
"UninstallOperation",
"UpdateOperation",
"BackupOperation",
]

View File

@@ -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

View File

@@ -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

View File

@@ -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