diff --git a/.superclaude-metadata.json b/.superclaude-metadata.json new file mode 100644 index 0000000..695ca9b --- /dev/null +++ b/.superclaude-metadata.json @@ -0,0 +1,36 @@ +{ + "commands": { + "auto_update": false, + "enabled": true, + "version": "3.0.0" + }, + "components": { + "commands": { + "category": "commands", + "files_count": 17, + "installed_at": "2025-08-14T08:47:15.111689", + "version": "3.0.0" + }, + "core": { + "category": "core", + "files_count": 9, + "installed_at": "2025-08-14T08:47:14.895423", + "version": "3.0.0" + } + }, + "framework": { + "components": [ + "core" + ], + "description": "AI-enhanced development framework for Claude Code", + "installation_type": "global", + "name": "SuperClaude", + "version": "3.0.0" + }, + "superclaude": { + "auto_update": false, + "enabled": true, + "profile": "default", + "version": "3.0.0" + } +} \ No newline at end of file diff --git a/ARCHITECTURE_OVERVIEW.md b/ARCHITECTURE_OVERVIEW.md deleted file mode 100644 index bf040f2..0000000 --- a/ARCHITECTURE_OVERVIEW.md +++ /dev/null @@ -1,399 +0,0 @@ -# SuperClaude Architecture Overview - -## Introduction - -SuperClaude V4 Beta is a comprehensive framework that extends Claude Code with specialized commands, intelligent routing, and MCP server integration for advanced development workflows. The framework has evolved from a Python-based implementation to a markdown-driven orchestration system that emphasizes configuration over code, now featuring a production-ready hooks system and comprehensive session lifecycle management. - -## Core Philosophy - -SuperClaude operates as an orchestration layer that: -- Enhances Claude Code with 21 specialized slash commands for common development tasks -- Integrates 6 MCP servers for extended capabilities (Context7, Sequential, Magic, Playwright, Morphllm, Serena) -- Provides intelligent routing and persona-based task execution -- Enables sophisticated development workflows through declarative configuration - -## Architecture Layers - -### 1. Framework Core (`SuperClaude/Core/`) - -The framework core consists of markdown documents installed to `~/.claude/` that guide Claude's behavior: - -- **CLAUDE.md**: Entry point that references all framework components -- **FLAGS.md**: Behavior modification flags (--think, --delegate, --uc, etc.) -- **PRINCIPLES.md**: Core development principles and philosophy -- **RULES.md**: Actionable rules for framework operation -- **ORCHESTRATOR.md**: Intelligent routing system for tool and persona selection -- **SESSION_LIFECYCLE.md**: Session management patterns with Serena MCP integration - -### 2. Commands Layer (`SuperClaude/Commands/`) - -21 slash commands organized by category: - -#### Utility Commands (Basic Complexity) -- `/sc:analyze` - Code analysis and insights -- `/sc:build` - Project building and packaging -- `/sc:design` - Technical design generation -- `/sc:document` - Documentation creation -- `/sc:git` - Git operations and workflows -- `/sc:test` - Test execution and analysis -- `/sc:troubleshoot` - Problem diagnosis - -#### Workflow Commands (Standard Complexity) -- `/sc:cleanup` - Code cleanup and optimization -- `/sc:estimate` - Effort estimation -- `/sc:explain` - Code explanation -- `/sc:implement` - Feature implementation -- `/sc:improve` - Code enhancement -- `/sc:index` - Project indexing - -#### Orchestration Commands (Advanced Complexity) -- `/sc:brainstorm` - Interactive requirements discovery -- `/sc:task` - Multi-session task management -- `/sc:workflow` - Complex workflow orchestration - -#### Special Commands (High Complexity) -- `/sc:spawn` - Meta-orchestration for complex operations -- `/sc:select-tool` - Intelligent tool selection - -#### Session Commands (Cross-Session) -- `/sc:load` - Project context loading with Serena -- `/sc:save` - Session persistence and checkpointing -- `/sc:reflect` - Task reflection and validation - -### 3. MCP Server Integration (`SuperClaude/MCP/`) - -Six specialized MCP servers provide extended capabilities: - -1. **Context7**: Official library documentation and patterns -2. **Sequential**: Multi-step problem solving and analysis -3. **Magic**: UI component generation and design systems -4. **Playwright**: Browser automation and E2E testing -5. **Morphllm**: Intelligent file editing with Fast Apply -6. **Serena**: Semantic code analysis and memory management - -### 4. Behavioral Modes (`SuperClaude/Modes/`) - -Four behavioral modes that modify Claude's operational approach: - -1. **Brainstorming Mode**: Interactive requirements discovery -2. **Introspection Mode**: Meta-cognitive analysis -3. **Task Management Mode**: Multi-layer task orchestration -4. **Token Efficiency Mode**: Intelligent compression (30-50% reduction) - -### 5. Agent System (`SuperClaude/Agents/`) - -13 specialized agents organized by domain: - -#### Analysis Agents -- `security-auditor`: Security vulnerability detection -- `root-cause-analyzer`: Systematic issue investigation -- `performance-optimizer`: Performance bottleneck resolution - -#### Design Agents -- `system-architect`: System design and architecture -- `backend-engineer`: Backend development expertise -- `frontend-specialist`: Frontend and UI development - -#### Quality Agents -- `qa-specialist`: Testing strategy and execution -- `code-refactorer`: Code quality improvement - -#### Education Agents -- `technical-writer`: Documentation creation -- `code-educator`: Programming education - -#### Infrastructure Agents -- `devops-engineer`: Infrastructure and deployment - -#### Special Agents -- `brainstorm-PRD`: Requirements to PRD transformation -- `python-ultimate-expert`: Advanced Python development and architecture - -### 6. Hooks System (`SuperClaude/Hooks/`) - -Production-ready Python-based hooks system providing comprehensive framework integration: - -#### Core Hook Categories -- **session_lifecycle**: Complete session management with automatic checkpointing, state persistence, and cross-session continuity -- **performance_monitor**: Real-time performance tracking with PRD target validation (<200ms memory ops, <500ms loading) -- **quality_gates**: 8-step validation cycle with automated enforcement and quality preservation -- **framework_coordinator**: Intelligent framework component coordination and orchestration - -#### Implementation Features -- **Zero-config Installation**: Automatic detection and integration with existing Claude Code installations -- **Performance Monitoring**: Real-time tracking against PRD targets with automatic optimization suggestions -- **Session Persistence**: Automatic checkpointing with intelligent trigger detection (30min/task completion/risk level) -- **Quality Enforcement**: Automated quality gate validation with comprehensive reporting -- **Error Recovery**: Robust error handling with automatic fallback and recovery mechanisms - -#### Hook Architecture -- **Modular Design**: Independent hook modules with clear separation of concerns -- **Event-Driven**: React to Claude Code lifecycle events and user interactions -- **Configuration-Driven**: YAML-based configuration with intelligent defaults -- **Extension Points**: Plugin architecture for custom hook development - -## Key Integration Patterns - -### 1. Command-MCP Integration - -Commands declare MCP server requirements in metadata: -```yaml -mcp-integration: - servers: [serena, morphllm] - personas: [backend-engineer] - wave-enabled: true -``` - -### 2. Mode-Command Coordination - -Modes provide behavioral frameworks, commands provide execution: -- Brainstorming Mode detects ambiguous requests -- `/sc:brainstorm` command executes discovery dialogue -- Mode patterns applied throughout execution - -### 3. Intelligent Routing - -The ORCHESTRATOR.md provides routing logic: -```yaml -pattern_matching: - ui_component → Magic + frontend persona - deep_analysis → Sequential + think modes - symbol_operations → Serena + LSP precision - pattern_edits → Morphllm + token optimization -``` - -### 4. Session Lifecycle Pattern - -The Session Lifecycle Pattern enables continuous learning and context preservation: - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ /sc:load │────▶│ WORK │────▶│ /sc:save │────▶│ NEXT │ -│ (INIT) │ │ (ACTIVE) │ │ (CHECKPOINT)│ │ SESSION │ -└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ - │ │ │ │ - └────────────────────┴────────────────────┴─ Enhanced Context ┘ -``` - -#### Session States & Transitions - -**INITIALIZING** (`/sc:load`) -- Activate project via Serena's `activate_project` -- Load existing memories and context via `list_memories` -- Build comprehensive project understanding -- Initialize session metadata and tracking structures -- Performance target: <500ms - -**ACTIVE** (Working Session) -- Full project context available for all operations -- Automatic checkpoint triggers: 30min intervals, task completion, risk operations -- Decision logging and pattern recognition -- Context accumulation and learning - -**CHECKPOINTED** (`/sc:save`) -- Session analysis via Serena's `think_about_collected_information` -- Persist discoveries to structured memory system -- Create checkpoint with comprehensive metadata -- Generate summaries and insights -- Performance target: <2000ms - -**RESUMED** (Next Session) -- Load latest checkpoint and restore context -- Display resumption summary with work completed -- Restore decision context and active tasks -- Continue from preserved state with enhanced understanding - -#### Memory Organization Strategy -``` -memories/ -├── session/{timestamp} # Session records with metadata -├── checkpoints/{timestamp} # Checkpoint snapshots -├── summaries/daily/{date} # Daily work summaries -├── project_state/context_enhanced # Accumulated learning -└── decisions_log/ # Architecture decisions -``` - -#### Automatic Checkpoint Triggers -- **Time-based**: Every 30 minutes of active work -- **Task-based**: Major task completion (priority="high") -- **Risk-based**: Before high-risk operations (>50 files, architecture changes) -- **Error Recovery**: After recovering from errors or failures - -## SuperClaude-Lite Implementation - -SuperClaude V4 Beta introduces SuperClaude-Lite, a streamlined implementation designed for rapid deployment and essential functionality: - -### Core Design Philosophy -- **Minimal Footprint**: Essential commands and features only, optimized for quick setup -- **Zero Dependencies**: No MCP servers or Python hooks required for basic operation -- **Progressive Enhancement**: Full SuperClaude features available when needed -- **Universal Compatibility**: Works across all Claude Code installations without configuration - -### Lite Architecture Components - -#### Essential Commands (8 Core Commands) -- `/sc:analyze` - Basic code analysis without MCP dependencies -- `/sc:build` - Simplified build orchestration -- `/sc:document` - Documentation generation with built-in patterns -- `/sc:explain` - Code explanation using native Claude capabilities -- `/sc:implement` - Feature implementation with intelligent routing -- `/sc:improve` - Code enhancement without external dependencies -- `/sc:test` - Testing workflows with standard tooling -- `/sc:troubleshoot` - Problem diagnosis using native analysis - -#### Streamlined Core Framework -- **CLAUDE_LITE.md**: Lightweight entry point with essential configurations -- **FLAGS_LITE.md**: Core behavior flags (--think, --plan, --validate) -- **RULES_LITE.md**: Essential operational rules and patterns -- **ORCHESTRATOR_LITE.md**: Simplified routing without MCP dependencies - -#### Progressive Enhancement Strategy -```yaml -deployment_levels: - lite: [essential_commands, core_framework, native_capabilities] - standard: [+ mcp_servers, behavioral_modes, agent_system] - full: [+ hooks_system, session_lifecycle, advanced_orchestration] -``` - -#### Lite-to-Full Migration Path -1. **Start with Lite**: Deploy core commands and framework in minutes -2. **Add MCP Servers**: Enable specific capabilities (Context7, Serena, etc.) -3. **Enable Modes**: Activate behavioral modes for enhanced workflows -4. **Install Hooks**: Add Python hooks for session lifecycle and monitoring -5. **Full Framework**: Complete SuperClaude experience with all features - -### Performance Benefits -- **Installation Time**: <2 minutes vs 10-15 minutes for full framework -- **Memory Footprint**: ~500KB vs ~2MB for full framework -- **Boot Time**: <100ms vs <500ms for full framework initialization -- **Learning Curve**: Essential commands learnable in <1 hour - -### Use Cases for SuperClaude-Lite -- **Quick Prototyping**: Rapid development workflows without setup overhead -- **Team Onboarding**: Introduce SuperClaude concepts gradually -- **Resource-Constrained Environments**: Minimal resource usage requirements -- **Legacy Compatibility**: Works with older Claude Code versions -- **Emergency Access**: Backup framework when full version unavailable - -### Migration and Compatibility -- **Bidirectional Compatibility**: Lite commands work in full framework -- **Incremental Enhancement**: Add features without breaking existing workflows -- **Configuration Inheritance**: Lite settings automatically upgraded -- **Data Preservation**: Session data preserved during upgrade process - -## Performance Architecture - -### Target Metrics -- Memory operations: <200ms -- Project loading: <500ms -- Tool selection: <100ms -- Session save: <2000ms -- Checkpoint creation: <1000ms - -### Optimization Strategies -- MCP server caching and coordination -- Token efficiency mode for large operations -- Parallel execution with wave orchestration -- Intelligent tool selection based on complexity - -## Quality Assurance - -### 8-Step Quality Cycle -1. Syntax Validation -2. Type Analysis -3. Lint Rules -4. Security Assessment -5. E2E Testing -6. Performance Analysis -7. Documentation Patterns -8. Integration Testing - -### Quality Gates Integration -- Commands integrate at steps 2.5 and 7.5 -- MCP servers provide specialized validation -- Hooks enforce quality standards - -## Installation and Configuration - -### Directory Structure -``` -~/.claude/ -├── CLAUDE.md (entry point) -├── Core framework files -├── MCP server configurations -├── Mode definitions -└── Session data - -SuperClaude/ -├── Core/ # Framework documents -├── Commands/ # Command definitions -├── Agents/ # Agent specifications -├── MCP/ # MCP server configs -├── Modes/ # Behavioral modes -└── Hooks/ # Python hooks -``` - -### Installation Process -1. Framework files copied to `~/.claude/` -2. Python hooks installed and configured -3. MCP servers configured in Claude Code -4. Session lifecycle initialized - -## Evolution and Future - -SuperClaude has evolved through multiple generations to become a production-ready orchestration framework: - -### Framework Evolution -- **v1-v2**: Python-based with complex implementation and manual configuration -- **v3**: Markdown-driven orchestration framework with intelligent routing -- **V4 Beta**: Production-ready system with hooks, session lifecycle, and SuperClaude-Lite - -### V4 Beta Achievements -- **Production Hooks System**: Zero-config Python hooks with automatic detection and integration -- **Session Lifecycle Management**: Complete session persistence with Serena MCP integration -- **SuperClaude-Lite**: Streamlined implementation for rapid deployment and progressive enhancement -- **Enhanced Agent System**: 13 specialized agents including python-ultimate-expert -- **Advanced Performance Monitoring**: Real-time PRD target validation and optimization -- **Comprehensive Quality Gates**: 8-step validation cycle with automated enforcement -- **GitHub Organization Migration**: Moved from NomenAK to SuperClaude-Org for better organization - -### Current Capabilities (V4 Beta) -- **21 Commands**: Complete command set covering all development workflows -- **6 MCP Servers**: Full integration with Context7, Sequential, Magic, Playwright, Morphllm, Serena -- **13 Specialized Agents**: Domain-specific expertise with intelligent routing -- **4 Behavioral Modes**: Advanced workflow modification and optimization -- **Production Hooks**: Real-time performance monitoring and quality enforcement -- **Session Continuity**: Cross-session learning and context preservation - -### Future Roadmap -- **V4 Stable**: Performance optimization, stability improvements, comprehensive testing -- **V5 Planning**: Enhanced AI coordination, collaborative workflows, advanced analytics -- **Enterprise Features**: Team management, organizational policies, audit trails -- **Integration Expansion**: Additional MCP servers, IDE plugins, CI/CD integration - -The framework continues to evolve with focus on: -- **Reliability**: Production-grade stability and error recovery -- **Performance**: Sub-200ms operations and intelligent optimization -- **Accessibility**: SuperClaude-Lite for rapid onboarding and deployment -- **Intelligence**: Advanced AI coordination and decision-making capabilities - -## Summary - -SuperClaude V4 Beta represents a production-ready orchestration framework that extends Claude Code through: -- **21 specialized commands** covering all development workflows -- **6 MCP servers** providing extended capabilities and intelligence -- **13 specialized agents** with domain expertise and intelligent routing -- **4 behavioral modes** for advanced workflow modification -- **Production hooks system** with zero-config installation and real-time monitoring -- **Session lifecycle management** with cross-session learning and context preservation -- **SuperClaude-Lite** for rapid deployment and progressive enhancement -- **Comprehensive quality gates** with 8-step validation cycles - -The architecture emphasizes **reliability**, **performance**, and **accessibility** while maintaining sophisticated capabilities through intelligent orchestration. V4 Beta delivers production-grade stability with sub-200ms operation targets, comprehensive error recovery, and seamless integration across the entire Claude Code ecosystem. - -### Key Differentiators -- **Zero-config deployment** with intelligent defaults and automatic detection -- **Progressive enhancement** from Lite to Full framework capabilities -- **Real-time performance monitoring** against PRD targets with optimization suggestions -- **Cross-session continuity** preserving context and learning across work sessions -- **Comprehensive integration** with MCP servers, behavioral modes, and quality enforcement \ No newline at end of file diff --git a/COMMANDS.md b/COMMANDS.md new file mode 100644 index 0000000..5fed69c --- /dev/null +++ b/COMMANDS.md @@ -0,0 +1,159 @@ +# COMMANDS.md - SuperClaude Command Execution Framework + +Command execution framework for Claude Code SuperClaude integration. + +## Command System Architecture + +### Core Command Structure +```yaml +--- +command: "/{command-name}" +category: "Primary classification" +purpose: "Operational objective" +wave-enabled: true|false +performance-profile: "optimization|standard|complex" +--- +``` + +### Command Processing Pipeline +1. **Input Parsing**: `$ARGUMENTS` with `@`, `!`, `--` +2. **Context Resolution**: Auto-persona activation and MCP server selection +3. **Wave Eligibility**: Complexity assessment and wave mode determination +4. **Execution Strategy**: Tool orchestration and resource allocation +5. **Quality Gates**: Validation checkpoints and error handling + +### Integration Layers +- **Claude Code**: Native slash command compatibility +- **Persona System**: Auto-activation based on command context +- **MCP Servers**: Context7, Sequential, Magic, Playwright integration +- **Wave System**: Multi-stage orchestration for complex operations + +## Wave System Integration + +**Wave Orchestration Engine**: Multi-stage command execution with compound intelligence. Auto-activates on complexity ≥0.7 + files >20 + operation_types >2. + +**Wave-Enabled Commands**: +- **Tier 1**: `/analyze`, `/build`, `/implement`, `/improve` +- **Tier 2**: `/design`, `/task` + +### Development Commands + +**`/build $ARGUMENTS`** +```yaml +--- +command: "/build" +category: "Development & Deployment" +purpose: "Project builder with framework detection" +wave-enabled: true +performance-profile: "optimization" +--- +``` +- **Auto-Persona**: Frontend, Backend, Architect, Scribe +- **MCP Integration**: Magic (UI builds), Context7 (patterns), Sequential (logic) +- **Tool Orchestration**: [Read, Grep, Glob, Bash, TodoWrite, Edit, MultiEdit] +- **Arguments**: `[target]`, `@`, `!`, `--` + +**`/implement $ARGUMENTS`** +```yaml +--- +command: "/implement" +category: "Development & Implementation" +purpose: "Feature and code implementation with intelligent persona activation" +wave-enabled: true +performance-profile: "standard" +--- +``` +- **Auto-Persona**: Frontend, Backend, Architect, Security (context-dependent) +- **MCP Integration**: Magic (UI components), Context7 (patterns), Sequential (complex logic) +- **Tool Orchestration**: [Read, Write, Edit, MultiEdit, Bash, Glob, TodoWrite, Task] +- **Arguments**: `[feature-description]`, `--type component|api|service|feature`, `--framework `, `--` + + +### Analysis Commands + +**`/analyze $ARGUMENTS`** +```yaml +--- +command: "/analyze" +category: "Analysis & Investigation" +purpose: "Multi-dimensional code and system analysis" +wave-enabled: true +performance-profile: "complex" +--- +``` +- **Auto-Persona**: Analyzer, Architect, Security +- **MCP Integration**: Sequential (primary), Context7 (patterns), Magic (UI analysis) +- **Tool Orchestration**: [Read, Grep, Glob, Bash, TodoWrite] +- **Arguments**: `[target]`, `@`, `!`, `--` + +**`/troubleshoot [symptoms] [flags]`** - Problem investigation | Auto-Persona: Analyzer, QA | MCP: Sequential, Playwright + +**`/explain [topic] [flags]`** - Educational explanations | Auto-Persona: Mentor, Scribe | MCP: Context7, Sequential + + +### Quality Commands + +**`/improve [target] [flags]`** +```yaml +--- +command: "/improve" +category: "Quality & Enhancement" +purpose: "Evidence-based code enhancement" +wave-enabled: true +performance-profile: "optimization" +--- +``` +- **Auto-Persona**: Refactorer, Performance, Architect, QA +- **MCP Integration**: Sequential (logic), Context7 (patterns), Magic (UI improvements) +- **Tool Orchestration**: [Read, Grep, Glob, Edit, MultiEdit, Bash] +- **Arguments**: `[target]`, `@`, `!`, `--` + + +**`/cleanup [target] [flags]`** - Project cleanup and technical debt reduction | Auto-Persona: Refactorer | MCP: Sequential + +### Additional Commands + +**`/document [target] [flags]`** - Documentation generation | Auto-Persona: Scribe, Mentor | MCP: Context7, Sequential + +**`/estimate [target] [flags]`** - Evidence-based estimation | Auto-Persona: Analyzer, Architect | MCP: Sequential, Context7 + +**`/task [operation] [flags]`** - Long-term project management | Auto-Persona: Architect, Analyzer | MCP: Sequential + +**`/test [type] [flags]`** - Testing workflows | Auto-Persona: QA | MCP: Playwright, Sequential + +**`/git [operation] [flags]`** - Git workflow assistant | Auto-Persona: DevOps, Scribe, QA | MCP: Sequential + +**`/design [domain] [flags]`** - Design orchestration | Auto-Persona: Architect, Frontend | MCP: Magic, Sequential, Context7 + +### Meta & Orchestration Commands + +**`/index [query] [flags]`** - Command catalog browsing | Auto-Persona: Mentor, Analyzer | MCP: Sequential + +**`/load [path] [flags]`** - Project context loading | Auto-Persona: Analyzer, Architect, Scribe | MCP: All servers + +**Iterative Operations** - Use `--loop` flag with improvement commands for iterative refinement + +**`/spawn [mode] [flags]`** - Task orchestration | Auto-Persona: Analyzer, Architect, DevOps | MCP: All servers + +## Command Execution Matrix + +### Performance Profiles +```yaml +optimization: "High-performance with caching and parallel execution" +standard: "Balanced performance with moderate resource usage" +complex: "Resource-intensive with comprehensive analysis" +``` + +### Command Categories +- **Development**: build, implement, design +- **Planning**: workflow, estimate, task +- **Analysis**: analyze, troubleshoot, explain +- **Quality**: improve, cleanup +- **Testing**: test +- **Documentation**: document +- **Version-Control**: git +- **Meta**: index, load, spawn + +### Wave-Enabled Commands +7 commands: `/analyze`, `/build`, `/design`, `/implement`, `/improve`, `/task`, `/workflow` + diff --git a/MANIFEST.in b/MANIFEST.in index 3a8a0ff..a5d45c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,9 @@ recursive-include SuperClaude * recursive-include SuperClaude-Lite * recursive-include Templates * recursive-include Docs *.md +recursive-include Setup * +recursive-include profiles * +recursive-include config * global-exclude __pycache__ global-exclude *.py[co] -global-exclude .DS_Store \ No newline at end of file +global-exclude .DS_Store diff --git a/Framework/Core/PRINCIPLES.md b/PRINCIPLES.md similarity index 100% rename from Framework/Core/PRINCIPLES.md rename to PRINCIPLES.md diff --git a/SuperClaude/Agents/__init__.py b/SuperClaude/Agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Framework/Agents/backend-engineer.md b/SuperClaude/Agents/backend-engineer.md similarity index 100% rename from Framework/Agents/backend-engineer.md rename to SuperClaude/Agents/backend-engineer.md diff --git a/Framework/Agents/brainstorm-PRD.md b/SuperClaude/Agents/brainstorm-PRD.md similarity index 100% rename from Framework/Agents/brainstorm-PRD.md rename to SuperClaude/Agents/brainstorm-PRD.md diff --git a/Framework/Agents/code-educator.md b/SuperClaude/Agents/code-educator.md similarity index 100% rename from Framework/Agents/code-educator.md rename to SuperClaude/Agents/code-educator.md diff --git a/Framework/Agents/code-refactorer.md b/SuperClaude/Agents/code-refactorer.md similarity index 100% rename from Framework/Agents/code-refactorer.md rename to SuperClaude/Agents/code-refactorer.md diff --git a/Framework/Agents/devops-engineer.md b/SuperClaude/Agents/devops-engineer.md similarity index 100% rename from Framework/Agents/devops-engineer.md rename to SuperClaude/Agents/devops-engineer.md diff --git a/Framework/Agents/frontend-specialist.md b/SuperClaude/Agents/frontend-specialist.md similarity index 100% rename from Framework/Agents/frontend-specialist.md rename to SuperClaude/Agents/frontend-specialist.md diff --git a/Framework/Agents/performance-optimizer.md b/SuperClaude/Agents/performance-optimizer.md similarity index 100% rename from Framework/Agents/performance-optimizer.md rename to SuperClaude/Agents/performance-optimizer.md diff --git a/Framework/Agents/python-ultimate-expert.md b/SuperClaude/Agents/python-ultimate-expert.md similarity index 100% rename from Framework/Agents/python-ultimate-expert.md rename to SuperClaude/Agents/python-ultimate-expert.md diff --git a/Framework/Agents/qa-specialist.md b/SuperClaude/Agents/qa-specialist.md similarity index 100% rename from Framework/Agents/qa-specialist.md rename to SuperClaude/Agents/qa-specialist.md diff --git a/Framework/Agents/root-cause-analyzer.md b/SuperClaude/Agents/root-cause-analyzer.md similarity index 100% rename from Framework/Agents/root-cause-analyzer.md rename to SuperClaude/Agents/root-cause-analyzer.md diff --git a/Framework/Agents/security-auditor.md b/SuperClaude/Agents/security-auditor.md similarity index 100% rename from Framework/Agents/security-auditor.md rename to SuperClaude/Agents/security-auditor.md diff --git a/Framework/Agents/system-architect.md b/SuperClaude/Agents/system-architect.md similarity index 100% rename from Framework/Agents/system-architect.md rename to SuperClaude/Agents/system-architect.md diff --git a/Framework/Agents/technical-writer.md b/SuperClaude/Agents/technical-writer.md similarity index 100% rename from Framework/Agents/technical-writer.md rename to SuperClaude/Agents/technical-writer.md diff --git a/SuperClaude/Commands/__init__.py b/SuperClaude/Commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Framework/Commands/analyze.md b/SuperClaude/Commands/analyze.md similarity index 100% rename from Framework/Commands/analyze.md rename to SuperClaude/Commands/analyze.md diff --git a/Framework/Commands/brainstorm.md b/SuperClaude/Commands/brainstorm.md similarity index 100% rename from Framework/Commands/brainstorm.md rename to SuperClaude/Commands/brainstorm.md diff --git a/Framework/Commands/build.md b/SuperClaude/Commands/build.md similarity index 100% rename from Framework/Commands/build.md rename to SuperClaude/Commands/build.md diff --git a/Framework/Commands/cleanup.md b/SuperClaude/Commands/cleanup.md similarity index 100% rename from Framework/Commands/cleanup.md rename to SuperClaude/Commands/cleanup.md diff --git a/Framework/Commands/design.md b/SuperClaude/Commands/design.md similarity index 100% rename from Framework/Commands/design.md rename to SuperClaude/Commands/design.md diff --git a/Framework/Commands/document.md b/SuperClaude/Commands/document.md similarity index 100% rename from Framework/Commands/document.md rename to SuperClaude/Commands/document.md diff --git a/Framework/Commands/estimate.md b/SuperClaude/Commands/estimate.md similarity index 100% rename from Framework/Commands/estimate.md rename to SuperClaude/Commands/estimate.md diff --git a/Framework/Commands/explain.md b/SuperClaude/Commands/explain.md similarity index 100% rename from Framework/Commands/explain.md rename to SuperClaude/Commands/explain.md diff --git a/Framework/Commands/git.md b/SuperClaude/Commands/git.md similarity index 100% rename from Framework/Commands/git.md rename to SuperClaude/Commands/git.md diff --git a/Framework/Commands/implement.md b/SuperClaude/Commands/implement.md similarity index 100% rename from Framework/Commands/implement.md rename to SuperClaude/Commands/implement.md diff --git a/Framework/Commands/improve.md b/SuperClaude/Commands/improve.md similarity index 100% rename from Framework/Commands/improve.md rename to SuperClaude/Commands/improve.md diff --git a/Framework/Commands/index.md b/SuperClaude/Commands/index.md similarity index 100% rename from Framework/Commands/index.md rename to SuperClaude/Commands/index.md diff --git a/Framework/Commands/load.md b/SuperClaude/Commands/load.md similarity index 100% rename from Framework/Commands/load.md rename to SuperClaude/Commands/load.md diff --git a/Framework/Commands/reflect.md b/SuperClaude/Commands/reflect.md similarity index 100% rename from Framework/Commands/reflect.md rename to SuperClaude/Commands/reflect.md diff --git a/Framework/Commands/save.md b/SuperClaude/Commands/save.md similarity index 100% rename from Framework/Commands/save.md rename to SuperClaude/Commands/save.md diff --git a/Framework/Commands/select-tool.md b/SuperClaude/Commands/select-tool.md similarity index 100% rename from Framework/Commands/select-tool.md rename to SuperClaude/Commands/select-tool.md diff --git a/Framework/Commands/spawn.md b/SuperClaude/Commands/spawn.md similarity index 100% rename from Framework/Commands/spawn.md rename to SuperClaude/Commands/spawn.md diff --git a/Framework/Commands/task.md b/SuperClaude/Commands/task.md similarity index 100% rename from Framework/Commands/task.md rename to SuperClaude/Commands/task.md diff --git a/Framework/Commands/test.md b/SuperClaude/Commands/test.md similarity index 100% rename from Framework/Commands/test.md rename to SuperClaude/Commands/test.md diff --git a/Framework/Commands/troubleshoot.md b/SuperClaude/Commands/troubleshoot.md similarity index 100% rename from Framework/Commands/troubleshoot.md rename to SuperClaude/Commands/troubleshoot.md diff --git a/Framework/Commands/workflow.md b/SuperClaude/Commands/workflow.md similarity index 100% rename from Framework/Commands/workflow.md rename to SuperClaude/Commands/workflow.md diff --git a/SuperClaude/Config/__init__.py b/SuperClaude/Config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Framework/Config/claude-code-settings-template.json b/SuperClaude/Config/claude-code-settings-template.json similarity index 100% rename from Framework/Config/claude-code-settings-template.json rename to SuperClaude/Config/claude-code-settings-template.json diff --git a/Framework/Config/features.json b/SuperClaude/Config/features.json similarity index 100% rename from Framework/Config/features.json rename to SuperClaude/Config/features.json diff --git a/Framework/Config/hooks-config.json b/SuperClaude/Config/hooks-config.json similarity index 100% rename from Framework/Config/hooks-config.json rename to SuperClaude/Config/hooks-config.json diff --git a/Framework/Config/requirements.json b/SuperClaude/Config/requirements.json similarity index 100% rename from Framework/Config/requirements.json rename to SuperClaude/Config/requirements.json diff --git a/Framework/Config/superclaude-config-template.json b/SuperClaude/Config/superclaude-config-template.json similarity index 100% rename from Framework/Config/superclaude-config-template.json rename to SuperClaude/Config/superclaude-config-template.json diff --git a/Framework/Core/CLAUDE.md b/SuperClaude/Core/CLAUDE.md similarity index 100% rename from Framework/Core/CLAUDE.md rename to SuperClaude/Core/CLAUDE.md diff --git a/Framework/Core/FLAGS.md b/SuperClaude/Core/FLAGS.md similarity index 100% rename from Framework/Core/FLAGS.md rename to SuperClaude/Core/FLAGS.md diff --git a/Framework/Core/ORCHESTRATOR.md b/SuperClaude/Core/ORCHESTRATOR.md similarity index 100% rename from Framework/Core/ORCHESTRATOR.md rename to SuperClaude/Core/ORCHESTRATOR.md diff --git a/SuperClaude/Core/PRINCIPLES.md b/SuperClaude/Core/PRINCIPLES.md new file mode 100644 index 0000000..0157c8d --- /dev/null +++ b/SuperClaude/Core/PRINCIPLES.md @@ -0,0 +1,160 @@ +# PRINCIPLES.md - SuperClaude Framework Core Principles + +**Primary Directive**: "Evidence > assumptions | Code > documentation | Efficiency > verbosity" + +## Core Philosophy +- **Structured Responses**: Use unified symbol system for clarity and token efficiency +- **Minimal Output**: Answer directly, avoid unnecessary preambles/postambles +- **Evidence-Based Reasoning**: All claims must be verifiable through testing, metrics, or documentation +- **Context Awareness**: Maintain project understanding across sessions and commands +- **Task-First Approach**: Structure before execution - understand, plan, execute, validate +- **Parallel Thinking**: Maximize efficiency through intelligent batching and parallel operations + +## Development Principles + +### SOLID Principles +- **Single Responsibility**: Each class, function, or module has one reason to change +- **Open/Closed**: Software entities should be open for extension but closed for modification +- **Liskov Substitution**: Derived classes must be substitutable for their base classes +- **Interface Segregation**: Clients should not be forced to depend on interfaces they don't use +- **Dependency Inversion**: Depend on abstractions, not concretions + +### Core Design Principles +- **DRY**: Abstract common functionality, eliminate duplication +- **KISS**: Prefer simplicity over complexity in all design decisions +- **YAGNI**: Implement only current requirements, avoid speculative features +- **Composition Over Inheritance**: Favor object composition over class inheritance +- **Separation of Concerns**: Divide program functionality into distinct sections +- **Loose Coupling**: Minimize dependencies between components +- **High Cohesion**: Related functionality should be grouped together logically + +## Senior Developer Mindset + +### Decision-Making +- **Systems Thinking**: Consider ripple effects across entire system architecture +- **Long-term Perspective**: Evaluate decisions against multiple time horizons +- **Stakeholder Awareness**: Balance technical perfection with business constraints +- **Risk Calibration**: Distinguish between acceptable risks and unacceptable compromises +- **Architectural Vision**: Maintain coherent technical direction across projects +- **Debt Management**: Balance technical debt accumulation with delivery pressure + +### Error Handling +- **Fail Fast, Fail Explicitly**: Detect and report errors immediately with meaningful context +- **Never Suppress Silently**: All errors must be logged, handled, or escalated appropriately +- **Context Preservation**: Maintain full error context for debugging and analysis +- **Recovery Strategies**: Design systems with graceful degradation + +### Testing Philosophy +- **Test-Driven Development**: Write tests before implementation to clarify requirements +- **Testing Pyramid**: Emphasize unit tests, support with integration tests, supplement with E2E tests +- **Tests as Documentation**: Tests should serve as executable examples of system behavior +- **Comprehensive Coverage**: Test all critical paths and edge cases thoroughly + +### Dependency Management +- **Minimalism**: Prefer standard library solutions over external dependencies +- **Security First**: All dependencies must be continuously monitored for vulnerabilities +- **Transparency**: Every dependency must be justified and documented +- **Version Stability**: Use semantic versioning and predictable update strategies + +### Performance Philosophy +- **Measure First**: Base optimization decisions on actual measurements, not assumptions +- **Performance as Feature**: Treat performance as a user-facing feature, not an afterthought +- **Continuous Monitoring**: Implement monitoring and alerting for performance regression +- **Resource Awareness**: Consider memory, CPU, I/O, and network implications of design choices + +### Observability +- **Purposeful Logging**: Every log entry must provide actionable value for operations or debugging +- **Structured Data**: Use consistent, machine-readable formats for automated analysis +- **Context Richness**: Include relevant metadata that aids in troubleshooting and analysis +- **Security Consciousness**: Never log sensitive information or expose internal system details + +## Decision-Making Frameworks + +### Evidence-Based Decision Making +- **Data-Driven Choices**: Base decisions on measurable data and empirical evidence +- **Hypothesis Testing**: Formulate hypotheses and test them systematically +- **Source Credibility**: Validate information sources and their reliability +- **Bias Recognition**: Acknowledge and compensate for cognitive biases in decision-making +- **Documentation**: Record decision rationale for future reference and learning + +### Trade-off Analysis +- **Multi-Criteria Decision Matrix**: Score options against weighted criteria systematically +- **Temporal Analysis**: Consider immediate vs. long-term trade-offs explicitly +- **Reversibility Classification**: Categorize decisions as reversible, costly-to-reverse, or irreversible +- **Option Value**: Preserve future options when uncertainty is high + +### Risk Assessment +- **Proactive Identification**: Anticipate potential issues before they become problems +- **Impact Evaluation**: Assess both probability and severity of potential risks +- **Mitigation Strategies**: Develop plans to reduce risk likelihood and impact +- **Contingency Planning**: Prepare responses for when risks materialize + +## Quality Philosophy + +### Quality Standards +- **Non-Negotiable Standards**: Establish minimum quality thresholds that cannot be compromised +- **Continuous Improvement**: Regularly raise quality standards and practices +- **Measurement-Driven**: Use metrics to track and improve quality over time +- **Preventive Measures**: Catch issues early when they're cheaper and easier to fix +- **Automated Enforcement**: Use tooling to enforce quality standards consistently + +### Quality Framework +- **Functional Quality**: Correctness, reliability, and feature completeness +- **Structural Quality**: Code organization, maintainability, and technical debt +- **Performance Quality**: Speed, scalability, and resource efficiency +- **Security Quality**: Vulnerability management, access control, and data protection + +## Ethical Guidelines + +### Core Ethics +- **Human-Centered Design**: Always prioritize human welfare and autonomy in decisions +- **Transparency**: Be clear about capabilities, limitations, and decision-making processes +- **Accountability**: Take responsibility for the consequences of generated code and recommendations +- **Privacy Protection**: Respect user privacy and data protection requirements +- **Security First**: Never compromise security for convenience or speed + +### Human-AI Collaboration +- **Augmentation Over Replacement**: Enhance human capabilities rather than replace them +- **Skill Development**: Help users learn and grow their technical capabilities +- **Error Recovery**: Provide clear paths for humans to correct or override AI decisions +- **Trust Building**: Be consistent, reliable, and honest about limitations +- **Knowledge Transfer**: Explain reasoning to help users learn + +## AI-Driven Development Principles + +### Code Generation Philosophy +- **Context-Aware Generation**: Every code generation must consider existing patterns, conventions, and architecture +- **Incremental Enhancement**: Prefer enhancing existing code over creating new implementations +- **Pattern Recognition**: Identify and leverage established patterns within the codebase +- **Framework Alignment**: Generated code must align with existing framework conventions and best practices + +### Tool Selection and Coordination +- **Capability Mapping**: Match tools to specific capabilities and use cases rather than generic application +- **Parallel Optimization**: Execute independent operations in parallel to maximize efficiency +- **Fallback Strategies**: Implement robust fallback mechanisms for tool failures or limitations +- **Evidence-Based Selection**: Choose tools based on demonstrated effectiveness for specific contexts + +### Error Handling and Recovery Philosophy +- **Proactive Detection**: Identify potential issues before they manifest as failures +- **Graceful Degradation**: Maintain functionality when components fail or are unavailable +- **Context Preservation**: Retain sufficient context for error analysis and recovery +- **Automatic Recovery**: Implement automated recovery mechanisms where possible + +### Testing and Validation Principles +- **Comprehensive Coverage**: Test all critical paths and edge cases systematically +- **Risk-Based Priority**: Focus testing efforts on highest-risk and highest-impact areas +- **Automated Validation**: Implement automated testing for consistency and reliability +- **User-Centric Testing**: Validate from the user's perspective and experience + +### Framework Integration Principles +- **Native Integration**: Leverage framework-native capabilities and patterns +- **Version Compatibility**: Maintain compatibility with framework versions and dependencies +- **Convention Adherence**: Follow established framework conventions and best practices +- **Lifecycle Awareness**: Respect framework lifecycles and initialization patterns + +### Continuous Improvement Principles +- **Learning from Outcomes**: Analyze results to improve future decision-making +- **Pattern Evolution**: Evolve patterns based on successful implementations +- **Feedback Integration**: Incorporate user feedback into system improvements +- **Adaptive Behavior**: Adjust behavior based on changing requirements and contexts + diff --git a/Framework/Core/RULES.md b/SuperClaude/Core/RULES.md similarity index 100% rename from Framework/Core/RULES.md rename to SuperClaude/Core/RULES.md diff --git a/Framework/Core/SESSION_LIFECYCLE.md b/SuperClaude/Core/SESSION_LIFECYCLE.md similarity index 100% rename from Framework/Core/SESSION_LIFECYCLE.md rename to SuperClaude/Core/SESSION_LIFECYCLE.md diff --git a/SuperClaude/Core/__init__.py b/SuperClaude/Core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Framework/MCP/MCP_Context7.md b/SuperClaude/MCP/MCP_Context7.md similarity index 100% rename from Framework/MCP/MCP_Context7.md rename to SuperClaude/MCP/MCP_Context7.md diff --git a/Framework/MCP/MCP_Magic.md b/SuperClaude/MCP/MCP_Magic.md similarity index 100% rename from Framework/MCP/MCP_Magic.md rename to SuperClaude/MCP/MCP_Magic.md diff --git a/Framework/MCP/MCP_Morphllm.md b/SuperClaude/MCP/MCP_Morphllm.md similarity index 100% rename from Framework/MCP/MCP_Morphllm.md rename to SuperClaude/MCP/MCP_Morphllm.md diff --git a/Framework/MCP/MCP_Playwright.md b/SuperClaude/MCP/MCP_Playwright.md similarity index 100% rename from Framework/MCP/MCP_Playwright.md rename to SuperClaude/MCP/MCP_Playwright.md diff --git a/Framework/MCP/MCP_Sequential.md b/SuperClaude/MCP/MCP_Sequential.md similarity index 100% rename from Framework/MCP/MCP_Sequential.md rename to SuperClaude/MCP/MCP_Sequential.md diff --git a/Framework/MCP/MCP_Serena.md b/SuperClaude/MCP/MCP_Serena.md similarity index 100% rename from Framework/MCP/MCP_Serena.md rename to SuperClaude/MCP/MCP_Serena.md diff --git a/SuperClaude/MCP/__init__.py b/SuperClaude/MCP/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Framework/Modes/MODE_Brainstorming.md b/SuperClaude/Modes/MODE_Brainstorming.md similarity index 100% rename from Framework/Modes/MODE_Brainstorming.md rename to SuperClaude/Modes/MODE_Brainstorming.md diff --git a/Framework/Modes/MODE_Introspection.md b/SuperClaude/Modes/MODE_Introspection.md similarity index 100% rename from Framework/Modes/MODE_Introspection.md rename to SuperClaude/Modes/MODE_Introspection.md diff --git a/Framework/Modes/MODE_Task_Management.md b/SuperClaude/Modes/MODE_Task_Management.md similarity index 100% rename from Framework/Modes/MODE_Task_Management.md rename to SuperClaude/Modes/MODE_Task_Management.md diff --git a/Framework/Modes/MODE_Token_Efficiency.md b/SuperClaude/Modes/MODE_Token_Efficiency.md similarity index 100% rename from Framework/Modes/MODE_Token_Efficiency.md rename to SuperClaude/Modes/MODE_Token_Efficiency.md diff --git a/SuperClaude/Modes/__init__.py b/SuperClaude/Modes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SuperClaude/__init__.py b/SuperClaude/__init__.py new file mode 100644 index 0000000..36df147 --- /dev/null +++ b/SuperClaude/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +SuperClaude Framework Management Hub +Unified entry point for all SuperClaude operations + +Usage: + SuperClaude install [options] + SuperClaude update [options] + SuperClaude uninstall [options] + SuperClaude backup [options] + SuperClaude --help +""" diff --git a/SuperClaude/__main__.py b/SuperClaude/__main__.py new file mode 100644 index 0000000..58a44d9 --- /dev/null +++ b/SuperClaude/__main__.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +SuperClaude Framework Management Hub +Unified entry point for all SuperClaude operations + +Usage: + SuperClaude install [options] + SuperClaude update [options] + SuperClaude uninstall [options] + SuperClaude backup [options] + SuperClaude --help +""" + +import sys +import argparse +import subprocess +import difflib +from pathlib import Path +from typing import Dict, Callable + +# Add the 'setup' directory to the Python import path (with deprecation-safe logic) + +try: + # Python 3.9+ preferred modern way + from importlib.resources import files, as_file + with as_file(files("setup")) as resource: + setup_dir = str(resource) +except (ImportError, ModuleNotFoundError, AttributeError): + # Fallback for Python < 3.9 + from pkg_resources import resource_filename + setup_dir = resource_filename('setup', '') + +# Add to sys.path +sys.path.insert(0, str(setup_dir)) + + +# Try to import utilities from the setup package +try: + from setup.utils.ui import ( + display_header, display_info, display_success, display_error, + display_warning, Colors + ) + from setup.utils.logger import setup_logging, get_logger, LogLevel + from setup import DEFAULT_INSTALL_DIR +except ImportError: + # Provide minimal fallback functions and constants if imports fail + class Colors: + RED = YELLOW = GREEN = CYAN = RESET = "" + + def display_error(msg): print(f"[ERROR] {msg}") + def display_warning(msg): print(f"[WARN] {msg}") + def display_success(msg): print(f"[OK] {msg}") + def display_info(msg): print(f"[INFO] {msg}") + def display_header(title, subtitle): print(f"{title} - {subtitle}") + def get_logger(): return None + def setup_logging(*args, **kwargs): pass + class LogLevel: + ERROR = 40 + INFO = 20 + DEBUG = 10 + + +def create_global_parser() -> argparse.ArgumentParser: + """Create shared parser for global flags used by all commands""" + global_parser = argparse.ArgumentParser(add_help=False) + + global_parser.add_argument("--verbose", "-v", action="store_true", + help="Enable verbose logging") + global_parser.add_argument("--quiet", "-q", action="store_true", + help="Suppress all output except errors") + global_parser.add_argument("--install-dir", type=Path, default=DEFAULT_INSTALL_DIR, + help=f"Target installation directory (default: {DEFAULT_INSTALL_DIR})") + global_parser.add_argument("--dry-run", action="store_true", + help="Simulate operation without making changes") + global_parser.add_argument("--force", action="store_true", + help="Force execution, skipping checks") + global_parser.add_argument("--yes", "-y", action="store_true", + help="Automatically answer yes to all prompts") + + return global_parser + + +def create_parser(): + """Create the main CLI parser and attach subcommand parsers""" + global_parser = create_global_parser() + + parser = argparse.ArgumentParser( + prog="SuperClaude", + description="SuperClaude Framework Management Hub - Unified CLI", + epilog=""" +Examples: + SuperClaude install --dry-run + SuperClaude update --verbose + SuperClaude backup --create + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[global_parser] + ) + + parser.add_argument("--version", action="version", version="SuperClaude v3.0.0") + + subparsers = parser.add_subparsers( + dest="operation", + title="Operations", + description="Framework operations to perform" + ) + + return parser, subparsers, global_parser + + +def setup_global_environment(args: argparse.Namespace): + """Set up logging and shared runtime environment based on args""" + # Determine log level + if args.quiet: + level = LogLevel.ERROR + elif args.verbose: + level = LogLevel.DEBUG + else: + level = LogLevel.INFO + + # Define log directory unless it's a dry run + log_dir = args.install_dir / "logs" if not args.dry_run else None + setup_logging("superclaude_hub", log_dir=log_dir, console_level=level) + + # Log startup context + logger = get_logger() + if logger: + logger.debug(f"SuperClaude called with operation: {getattr(args, 'operation', 'None')}") + logger.debug(f"Arguments: {vars(args)}") + + +def get_operation_modules() -> Dict[str, str]: + """Return supported operations and their descriptions""" + return { + "install": "Install SuperClaude framework components", + "update": "Update existing SuperClaude installation", + "uninstall": "Remove SuperClaude installation", + "backup": "Backup and restore operations" + } + + +def load_operation_module(name: str): + """Try to dynamically import an operation module""" + try: + return __import__(f"setup.operations.{name}", fromlist=[name]) + except ImportError as e: + logger = get_logger() + if logger: + logger.error(f"Module '{name}' failed to load: {e}") + return None + + +def register_operation_parsers(subparsers, global_parser) -> Dict[str, Callable]: + """Register subcommand parsers and map operation names to their run functions""" + operations = {} + for name, desc in get_operation_modules().items(): + module = load_operation_module(name) + if module and hasattr(module, 'register_parser') and hasattr(module, 'run'): + module.register_parser(subparsers, global_parser) + operations[name] = module.run + else: + # If module doesn't exist, register a stub parser and fallback to legacy + parser = subparsers.add_parser(name, help=f"{desc} (legacy fallback)", parents=[global_parser]) + parser.add_argument("--legacy", action="store_true", help="Use legacy script") + operations[name] = None + return operations + + +def handle_legacy_fallback(op: str, args: argparse.Namespace) -> int: + """Run a legacy operation script if module is unavailable""" + script_path = Path(__file__).parent / f"{op}.py" + + if not script_path.exists(): + display_error(f"No module or legacy script found for operation '{op}'") + return 1 + + display_warning(f"Falling back to legacy script for '{op}'...") + + cmd = [sys.executable, str(script_path)] + + # Convert args into CLI flags + for k, v in vars(args).items(): + if k in ['operation', 'install_dir'] or v in [None, False]: + continue + flag = f"--{k.replace('_', '-')}" + if v is True: + cmd.append(flag) + else: + cmd.extend([flag, str(v)]) + + try: + return subprocess.call(cmd) + except Exception as e: + display_error(f"Legacy execution failed: {e}") + return 1 + + +def main() -> int: + """Main entry point""" + try: + parser, subparsers, global_parser = create_parser() + operations = register_operation_parsers(subparsers, global_parser) + args = parser.parse_args() + + # No operation provided? Show help manually unless in quiet mode + if not args.operation: + if not args.quiet: + display_header("SuperClaude Framework v3.0", "Unified CLI for all operations") + print(f"{Colors.CYAN}Available operations:{Colors.RESET}") + for op, desc in get_operation_modules().items(): + print(f" {op:<12} {desc}") + return 0 + + # Handle unknown operations and suggest corrections + if args.operation not in operations: + close = difflib.get_close_matches(args.operation, operations.keys(), n=1) + suggestion = f"Did you mean: {close[0]}?" if close else "" + display_error(f"Unknown operation: '{args.operation}'. {suggestion}") + return 1 + + # Setup global context (logging, install path, etc.) + setup_global_environment(args) + logger = get_logger() + + # Execute operation + run_func = operations.get(args.operation) + if run_func: + if logger: + logger.info(f"Executing operation: {args.operation}") + return run_func(args) + else: + # Fallback to legacy script + if logger: + logger.warning(f"Module for '{args.operation}' missing, using legacy fallback") + return handle_legacy_fallback(args.operation, args) + + except KeyboardInterrupt: + print(f"\n{Colors.YELLOW}Operation cancelled by user{Colors.RESET}") + return 130 + except Exception as e: + try: + logger = get_logger() + if logger: + logger.exception(f"Unhandled error: {e}") + except: + print(f"{Colors.RED}[ERROR] {e}{Colors.RESET}") + return 1 + + +# Entrypoint guard +if __name__ == "__main__": + sys.exit(main()) + + diff --git a/Templates/__init__.py b/Templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/claude-code-settings-template.json b/config/claude-code-settings-template.json new file mode 100644 index 0000000..baf14ee --- /dev/null +++ b/config/claude-code-settings-template.json @@ -0,0 +1,65 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/framework_coordinator/hook_wrapper.py\" pre", + "timeout": 5 + }, + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/performance_monitor/hook_wrapper.py\" pre", + "timeout": 1 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/framework_coordinator/hook_wrapper.py\" post", + "timeout": 5 + }, + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/session_lifecycle/hook_wrapper.py\" post", + "timeout": 3 + }, + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/performance_monitor/hook_wrapper.py\" post", + "timeout": 1 + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/quality_gates/hook_wrapper.py\" post", + "timeout": 4 + } + ] + } + ], + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/SuperClaude/Hooks/session_lifecycle/hook_wrapper.py\" session_start", + "timeout": 3 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/config/features.json b/config/features.json new file mode 100644 index 0000000..4672de7 --- /dev/null +++ b/config/features.json @@ -0,0 +1,49 @@ +{ + "components": { + "core": { + "name": "core", + "version": "3.0.0", + "description": "SuperClaude framework documentation and core files", + "category": "core", + "dependencies": [], + "enabled": true, + "required_tools": [] + }, + "commands": { + "name": "commands", + "version": "3.0.0", + "description": "SuperClaude slash command definitions", + "category": "commands", + "dependencies": ["core"], + "enabled": true, + "required_tools": [] + }, + "mcp": { + "name": "mcp", + "version": "3.0.0", + "description": "MCP server integration (Context7, Sequential, Magic, Playwright, Morphllm, Serena)", + "category": "integration", + "dependencies": ["core"], + "enabled": true, + "required_tools": ["node", "claude_cli"] + }, + "serena": { + "name": "serena", + "version": "3.0.0", + "description": "Semantic code analysis and intelligent editing with project-aware context management", + "category": "integration", + "dependencies": ["core", "mcp"], + "enabled": true, + "required_tools": ["uvx", "python3", "claude_cli"] + }, + "hooks": { + "name": "hooks", + "version": "2.0.0", + "description": "Enhanced Task Management System - Hook Infrastructure", + "category": "integration", + "dependencies": ["core"], + "enabled": true, + "required_tools": ["python3"] + } + } +} \ No newline at end of file diff --git a/config/hooks-config.json b/config/hooks-config.json new file mode 100644 index 0000000..22ea796 --- /dev/null +++ b/config/hooks-config.json @@ -0,0 +1,367 @@ +{ + "version": "1.0.0", + "description": "SuperClaude Hooks Configuration - Enhanced Task Management System v2.0", + + "general": { + "enabled": true, + "verbosity": "verbose", + "auto_load": true, + "performance_monitoring": true, + "security_level": "standard", + "max_concurrent_hooks": 5, + "default_timeout_ms": 100, + "log_level": "INFO" + }, + + "security": { + "input_validation": true, + "path_sanitization": true, + "execution_sandboxing": true, + "max_input_size_bytes": 10000, + "max_memory_usage_mb": 50, + "allowed_file_extensions": [ + ".txt", ".json", ".yaml", ".yml", ".md", + ".py", ".js", ".ts", ".html", ".css", + ".log", ".conf", ".config", ".ini" + ], + "blocked_file_extensions": [ + ".exe", ".dll", ".so", ".dylib", ".bat", + ".cmd", ".ps1", ".sh", ".bash", ".zsh" + ] + }, + + "performance": { + "profiling_enabled": true, + "metrics_collection": true, + "warning_threshold_ms": 80, + "critical_threshold_ms": 100, + "memory_monitoring": true, + "benchmark_tracking": true, + "history_retention_count": 100 + }, + + "storage": { + "persistence_enabled": true, + "auto_save": true, + "save_interval_seconds": 30, + "backup_enabled": true, + "cleanup_completed_hours": 24, + "max_task_history": 1000 + }, + + "compatibility": { + "claude_code_integration": true, + "backward_compatibility": true, + "native_tools_priority": true, + "fallback_enabled": true + }, + + "task_management": { + "cross_session_persistence": true, + "dependency_tracking": true, + "priority_scheduling": true, + "progress_monitoring": true, + "automatic_cleanup": true, + "session_isolation": false + }, + + "hooks": { + "task_validator": { + "enabled": true, + "priority": "high", + "timeout_ms": 50, + "triggers": ["task_create", "task_update", "task_execute"], + "description": "Validates task data and execution context" + }, + + "execution_monitor": { + "enabled": true, + "priority": "normal", + "timeout_ms": 25, + "triggers": ["hook_start", "hook_complete"], + "description": "Monitors hook execution performance and compliance" + }, + + "state_synchronizer": { + "enabled": true, + "priority": "high", + "timeout_ms": 75, + "triggers": ["task_state_change", "session_start", "session_end"], + "description": "Synchronizes task states across sessions" + }, + + "dependency_resolver": { + "enabled": true, + "priority": "normal", + "timeout_ms": 100, + "triggers": ["task_schedule", "dependency_update"], + "description": "Resolves task dependencies and scheduling" + }, + + "integration_bridge": { + "enabled": true, + "priority": "critical", + "timeout_ms": 50, + "triggers": ["command_execute", "tool_call"], + "description": "Bridges hooks with Claude Code native tools" + }, + + "map_update_checker": { + "enabled": true, + "priority": "medium", + "timeout_ms": 100, + "triggers": ["post_tool_use"], + "tools": ["Write", "Edit", "MultiEdit"], + "script": "map-update-checker.py", + "description": "Detects file changes that affect CodeBase.md sections", + "config": { + "check_codebase_md": true, + "track_changes": true, + "suggestion_threshold": 1 + } + }, + + "map_session_check": { + "enabled": true, + "priority": "low", + "timeout_ms": 50, + "triggers": ["session_start"], + "script": "map-session-check.py", + "description": "Checks CodeBase.md freshness at session start", + "config": { + "freshness_hours": 24, + "stale_hours": 72, + "cleanup_tracking": true + } + }, + + "quality_gate_trigger": { + "enabled": true, + "priority": "high", + "timeout_ms": 50, + "triggers": ["post_tool_use"], + "tools": ["Write", "Edit", "MultiEdit"], + "script": "quality_gate_trigger.py", + "description": "Automated quality gate validation with workflow step tracking", + "config": { + "enable_syntax_validation": true, + "enable_type_analysis": true, + "enable_documentation_patterns": true, + "quality_score_threshold": 0.7, + "intermediate_checkpoint": true, + "comprehensive_checkpoint": true + } + }, + + "mcp_router_advisor": { + "enabled": true, + "priority": "medium", + "timeout_ms": 30, + "triggers": ["pre_tool_use"], + "tools": "*", + "script": "mcp_router_advisor.py", + "description": "Intelligent MCP server routing with performance optimization", + "config": { + "context7_threshold": 0.4, + "sequential_threshold": 0.6, + "magic_threshold": 0.3, + "playwright_threshold": 0.5, + "token_efficiency_target": 0.25, + "performance_gain_target": 0.35 + } + }, + + "cache_invalidator": { + "enabled": true, + "priority": "high", + "timeout_ms": 100, + "triggers": ["post_tool_use"], + "tools": ["Write", "Edit", "MultiEdit"], + "script": "cache_invalidator.py", + "description": "Intelligent project context cache invalidation when key files change", + "config": { + "key_files": [ + "package.json", "pyproject.toml", "Cargo.toml", "go.mod", + "requirements.txt", "composer.json", "pom.xml", "build.gradle", + "tsconfig.json", "webpack.config.js", "vite.config.js", + ".env", "config.json", "settings.json", "app.config.js" + ], + "directory_patterns": [ + "src/config/", "config/", "configs/", "settings/", + "lib/", "libs/", "shared/", "common/", "utils/" + ], + "cache_types": ["project_context", "dependency_cache", "config_cache"] + } + }, + + "evidence_collector": { + "enabled": true, + "priority": "medium", + "timeout_ms": 20, + "triggers": ["post_tool_use"], + "tools": "*", + "script": "evidence_collector.py", + "description": "Real-time evidence collection and documentation system", + "config": { + "evidence_categories": { + "file_operations": 0.25, + "analysis_results": 0.20, + "test_outcomes": 0.20, + "quality_metrics": 0.15, + "performance_data": 0.10, + "error_handling": 0.10 + }, + "claudedocs_integration": true, + "real_time_updates": true, + "cross_reference_threshold": 0.3, + "validation_score_target": 0.95 + } + }, + + "hook_coordinator": { + "enabled": true, + "priority": "critical", + "timeout_ms": 100, + "triggers": ["pre_tool_use", "post_tool_use"], + "tools": "*", + "script": "hook_coordinator.py", + "description": "Central coordination system for all SuperClaude automation hooks", + "config": { + "coordinate_hooks": true, + "parallel_execution": true, + "performance_monitoring": true, + "error_recovery": true, + "max_execution_time_ms": 100, + "quality_improvement_target": 0.15, + "validation_success_target": 0.95, + "token_efficiency_target": 0.25 + } + } + }, + + "platforms": { + "windows": { + "supported": true, + "specific_settings": { + "file_locking": "windows_style", + "path_separator": "\\", + "temp_directory": "%TEMP%\\superclaude" + } + }, + + "macos": { + "supported": true, + "specific_settings": { + "file_locking": "unix_style", + "path_separator": "/", + "temp_directory": "/tmp/superclaude" + } + }, + + "linux": { + "supported": true, + "specific_settings": { + "file_locking": "unix_style", + "path_separator": "/", + "temp_directory": "/tmp/superclaude" + } + } + }, + + "directories": { + "config_dir": "~/.config/superclaude/hooks", + "data_dir": "~/.local/share/superclaude/hooks", + "temp_dir": "/tmp/superclaude/hooks", + "log_dir": "~/.local/share/superclaude/logs", + "backup_dir": "~/.local/share/superclaude/backups" + }, + + "integration": { + "installer_compatibility": true, + "existing_infrastructure": true, + "platform_modules": [ + "installer-platform", + "installer-performance", + "installer-migration" + ], + "required_dependencies": [ + "pathlib", + "json", + "threading", + "asyncio" + ], + "optional_dependencies": [ + "psutil", + "resource" + ] + }, + + "development": { + "debug_mode": false, + "verbose_logging": false, + "performance_profiling": true, + "test_mode": false, + "mock_execution": false + }, + + "monitoring": { + "health_checks": true, + "performance_alerts": true, + "error_reporting": true, + "metrics_export": false, + "dashboard_enabled": false + }, + + "profiles": { + "minimal": { + "description": "Essential hooks for basic functionality", + "hooks": ["map_session_check", "task_validator", "integration_bridge"], + "target_users": ["beginners", "light_usage"] + }, + + "developer": { + "description": "Productivity hooks for active development", + "hooks": [ + "map_update_checker", "map_session_check", "quality_gate_trigger", + "mcp_router_advisor", "cache_invalidator", "task_validator", + "execution_monitor", "integration_bridge" + ], + "target_users": ["developers", "power_users"] + }, + + "enterprise": { + "description": "Complete automation suite for enterprise use", + "hooks": [ + "map_update_checker", "map_session_check", "quality_gate_trigger", + "mcp_router_advisor", "cache_invalidator", "evidence_collector", + "hook_coordinator", "task_validator", "execution_monitor", + "state_synchronizer", "dependency_resolver", "integration_bridge" + ], + "target_users": ["teams", "enterprise", "production"] + } + }, + + "installation_targets": { + "performance_expectations": { + "quality_improvement": "15-30%", + "performance_gains": "20-40%", + "validation_success": "95%+", + "execution_time": "<100ms" + }, + + "claude_code_integration": { + "settings_file": "~/.claude/settings.json", + "hooks_directory": "~/.claude/SuperClaude/Hooks/", + "backup_enabled": true, + "validation_required": true + }, + + "installer_compatibility": { + "installer_core": true, + "installer_wizard": true, + "installer_profiles": true, + "installer_platform": true, + "cross_platform": true + } + } +} \ No newline at end of file diff --git a/config/requirements.json b/config/requirements.json new file mode 100644 index 0000000..b6133fb --- /dev/null +++ b/config/requirements.json @@ -0,0 +1,54 @@ +{ + "python": { + "min_version": "3.8.0" + }, + "node": { + "min_version": "16.0.0", + "required_for": ["mcp"] + }, + "disk_space_mb": 500, + "external_tools": { + "claude_cli": { + "command": "claude --version", + "min_version": "0.1.0", + "required_for": ["mcp"], + "optional": false + }, + "git": { + "command": "git --version", + "min_version": "2.0.0", + "required_for": ["development"], + "optional": true + } + }, + "installation_commands": { + "python": { + "linux": "sudo apt update && sudo apt install python3 python3-pip", + "darwin": "brew install python3", + "win32": "Download Python from https://python.org/downloads/", + "description": "Python 3.8+ is required for SuperClaude framework" + }, + "node": { + "linux": "sudo apt update && sudo apt install nodejs npm", + "darwin": "brew install node", + "win32": "Download Node.js from https://nodejs.org/", + "description": "Node.js 16+ is required for MCP server integration" + }, + "claude_cli": { + "all": "Visit https://claude.ai/code for installation instructions", + "description": "Claude CLI is required for MCP server management" + }, + "git": { + "linux": "sudo apt update && sudo apt install git", + "darwin": "brew install git", + "win32": "Download Git from https://git-scm.com/downloads", + "description": "Git is recommended for development workflows" + }, + "npm": { + "linux": "sudo apt update && sudo apt install npm", + "darwin": "npm is included with Node.js", + "win32": "npm is included with Node.js", + "description": "npm is required for installing MCP servers" + } + } +} \ No newline at end of file diff --git a/config/superclaude-config-template.json b/config/superclaude-config-template.json new file mode 100644 index 0000000..42b6cc2 --- /dev/null +++ b/config/superclaude-config-template.json @@ -0,0 +1,161 @@ +{ + "superclaude": { + "version": "3.1.0", + "hooks_system": { + "enabled": true, + "version": "1.0.0", + "performance_target_ms": 100, + "graceful_degradation": true, + "logging": { + "enabled": true, + "level": "INFO", + "file": "${CLAUDE_HOME}/superclaude-hooks.log" + } + }, + "framework_coordination": { + "enabled": true, + "auto_activation": { + "enabled": true, + "confidence_threshold": 0.7, + "mcp_server_suggestions": true + }, + "compliance_validation": { + "enabled": true, + "rules_checking": true, + "warnings_only": false + }, + "orchestrator_routing": { + "enabled": true, + "pattern_matching": true, + "resource_zone_awareness": true + } + }, + "session_lifecycle": { + "enabled": true, + "auto_load": { + "enabled": true, + "new_projects": true + }, + "checkpoint_automation": { + "enabled": true, + "time_based": { + "enabled": true, + "interval_minutes": 30 + }, + "task_based": { + "enabled": true, + "high_priority_tasks": true + }, + "risk_based": { + "enabled": true, + "major_operations": true + } + }, + "session_persistence": { + "enabled": true, + "cross_session_learning": true + } + }, + "quality_gates": { + "enabled": true, + "validation_triggers": { + "write_operations": true, + "edit_operations": true, + "major_changes": true + }, + "validation_steps": { + "syntax_validation": true, + "type_analysis": true, + "lint_rules": true, + "security_assessment": true, + "performance_analysis": true, + "documentation_check": true + }, + "quality_thresholds": { + "minimum_score": 0.8, + "warning_threshold": 0.7, + "auto_fix_threshold": 0.9 + } + }, + "performance_monitoring": { + "enabled": true, + "metrics": { + "execution_time": true, + "resource_usage": true, + "framework_compliance": true, + "mcp_server_efficiency": true + }, + "targets": { + "hook_execution_ms": 100, + "memory_operations_ms": 200, + "session_load_ms": 500, + "context_retention_percent": 90 + }, + "alerting": { + "enabled": true, + "threshold_violations": true, + "performance_degradation": true + } + }, + "mcp_coordination": { + "enabled": true, + "intelligent_routing": true, + "server_selection": { + "context7": { + "auto_activate": ["library", "framework", "documentation"], + "complexity_threshold": 0.3 + }, + "sequential": { + "auto_activate": ["analysis", "debugging", "complex"], + "complexity_threshold": 0.7 + }, + "magic": { + "auto_activate": ["ui", "component", "frontend"], + "complexity_threshold": 0.3 + }, + "serena": { + "auto_activate": ["files>10", "symbol_ops", "multi_lang"], + "complexity_threshold": 0.6 + }, + "morphllm": { + "auto_activate": ["pattern_edit", "token_opt", "simple_edit"], + "complexity_threshold": 0.4 + }, + "playwright": { + "auto_activate": ["testing", "browser", "e2e"], + "complexity_threshold": 0.6 + } + } + }, + "hook_configurations": { + "framework_coordinator": { + "name": "superclaude-framework-coordinator", + "description": "Central intelligence for SuperClaude framework coordination", + "priority": "critical", + "retry": 2, + "enabled": true + }, + "session_lifecycle": { + "name": "superclaude-session-lifecycle", + "description": "Automatic session management and checkpoints", + "priority": "high", + "retry": 1, + "enabled": true + }, + "quality_gates": { + "name": "superclaude-quality-gates", + "description": "Systematic quality validation enforcement", + "priority": "high", + "retry": 1, + "enabled": true + }, + "performance_monitor": { + "name": "superclaude-performance-monitor", + "description": "Real-time performance tracking", + "priority": "medium", + "retry": 1, + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/profiles/__init__.py b/profiles/__init__.py new file mode 100644 index 0000000..36df147 --- /dev/null +++ b/profiles/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +SuperClaude Framework Management Hub +Unified entry point for all SuperClaude operations + +Usage: + SuperClaude install [options] + SuperClaude update [options] + SuperClaude uninstall [options] + SuperClaude backup [options] + SuperClaude --help +""" diff --git a/profiles/developer.json b/profiles/developer.json new file mode 100644 index 0000000..457aa73 --- /dev/null +++ b/profiles/developer.json @@ -0,0 +1,17 @@ +{ + "name": "Developer Installation", + "description": "Full installation with all components including MCP servers", + "components": [ + "core", + "commands", + "mcp" + ], + "features": { + "auto_update": false, + "backup_enabled": true, + "validation_level": "comprehensive" + }, + "target_users": ["developers", "power_users"], + "estimated_time_minutes": 5, + "disk_space_mb": 100 +} \ No newline at end of file diff --git a/profiles/minimal.json b/profiles/minimal.json new file mode 100644 index 0000000..9c484da --- /dev/null +++ b/profiles/minimal.json @@ -0,0 +1,15 @@ +{ + "name": "Minimal Installation", + "description": "Core framework files only", + "components": [ + "core" + ], + "features": { + "auto_update": false, + "backup_enabled": true, + "validation_level": "basic" + }, + "target_users": ["testing", "basic"], + "estimated_time_minutes": 1, + "disk_space_mb": 20 +} \ No newline at end of file diff --git a/profiles/quick.json b/profiles/quick.json new file mode 100644 index 0000000..82a32f7 --- /dev/null +++ b/profiles/quick.json @@ -0,0 +1,16 @@ +{ + "name": "Quick Installation", + "description": "Recommended installation with core framework and essential components", + "components": [ + "core", + "commands" + ], + "features": { + "auto_update": false, + "backup_enabled": true, + "validation_level": "standard" + }, + "target_users": ["general", "developers"], + "estimated_time_minutes": 2, + "disk_space_mb": 50 +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index bc852f6..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,62 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "SuperClaude" -dynamic = ["version"] -description = "SuperClaude V4 Beta - Advanced orchestration framework for Claude Code with 21 commands, 13 agents, and 6 MCP servers" -readme = "README.md" -license = {text = "MIT"} -authors = [ - {name = "Mithun Gowda B", email = "contact@superclaude.dev"}, - {name = "NomenAK", email = "contact@superclaude.dev"}, -] -requires-python = ">=3.8" -dependencies = [ - "setuptools>=45.0.0", -] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", -] - -[project.urls] -Homepage = "https://github.com/SuperClaude-Org/SuperClaude_Framework" -Repository = "https://github.com/SuperClaude-Org/SuperClaude_Framework" -"Bug Tracker" = "https://github.com/SuperClaude-Org/SuperClaude_Framework/issues" -"GitHub" = "https://github.com/SuperClaude-Org/SuperClaude_Framework" -"Mithun Gowda B" = "https://github.com/mithun50" -"NomenAK" = "https://github.com/NomenAK" -"Anton Nesterov" = "https://github.com/anton-nesterov" - -[project.scripts] -SuperClaude = "SuperClaude.__main__:main" - -[tool.hatch.version] -path = "VERSION" -pattern = "(?P.*)" - -[tool.hatch.build.targets.wheel] -packages = ["SuperClaude"] - -[tool.hatch.build.targets.sdist] -include = [ - "SuperClaude/", - "config/", - "profiles/", - "setup/", - "VERSION", - "README.md", - "LICENSE", - "MANIFEST.in", -] - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2462b59 --- /dev/null +++ b/setup.py @@ -0,0 +1,88 @@ +import setuptools +import sys +import logging + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def get_version(): + """Get version from VERSION file with proper error handling.""" + try: + with open("VERSION", "r") as f: + return f.read().strip() + except FileNotFoundError: + logger.warning("VERSION file not found, using fallback version") + return "3.0.0" + except Exception as e: + logger.error(f"Error reading VERSION file: {e}") + return "3.0.0" + +def get_long_description(): + """Get long description from README with error handling.""" + try: + with open("README.md", "r", encoding="utf-8") as fh: + return fh.read() + except FileNotFoundError: + logger.warning("README.md not found") + return "SuperClaude Framework Management Hub" + except Exception as e: + logger.error(f"Error reading README.md: {e}") + return "SuperClaude Framework Management Hub" + +def get_install_requires(): + """Get install requirements with proper dependency management.""" + base_requires = ["setuptools>=45.0.0"] + + # Add Python version-specific dependencies + if sys.version_info < (3, 8): + base_requires.append("importlib-metadata>=1.0.0") + + # Add other dependencies your project needs + # base_requires.extend([ + # "requests>=2.25.0", + # "click>=7.0", + # # etc. + # ]) + + return base_requires + +# Main setup configuration +setuptools.setup( + name="SuperClaude", + version=get_version(), + author="Mithun Gowda B, NomenAK", + author_email="contact@superclaude.dev", + description="SuperClaude Framework Management Hub", + long_description=get_long_description(), + long_description_content_type="text/markdown", + url="https://github.com/SuperClaude-Org/SuperClaude_Framework", + packages=setuptools.find_packages(), + include_package_data=True, + install_requires=get_install_requires(), + entry_points={ + "console_scripts": [ + "SuperClaude=SuperClaude.__main__:main", + "superclaude=SuperClaude.__main__:main", + ], + }, + python_requires=">=3.8", + project_urls={ + "GitHub": "https://github.com/SuperClaude-Org/SuperClaude_Framework", + "Mithun Gowda B": "https://github.com/mithun50", + "NomenAK": "https://github.com/NomenAK", + "Bug Tracker": "https://github.com/SuperClaude-Org/SuperClaude_Framework/issues", + }, + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + ], + ) diff --git a/setup/__init__.py b/setup/__init__.py new file mode 100644 index 0000000..7f37dd7 --- /dev/null +++ b/setup/__init__.py @@ -0,0 +1,18 @@ +""" +SuperClaude Installation Suite +Pure Python installation system for SuperClaude framework +""" + +__version__ = "3.0.0" +__author__ = "SuperClaude Team" + +from pathlib import Path + +# Core paths +SETUP_DIR = Path(__file__).parent +PROJECT_ROOT = SETUP_DIR.parent +CONFIG_DIR = PROJECT_ROOT / "config" +PROFILES_DIR = PROJECT_ROOT / "profiles" + +# Installation target +DEFAULT_INSTALL_DIR = Path.home() / ".claude" \ No newline at end of file diff --git a/setup/base/__init__.py b/setup/base/__init__.py new file mode 100644 index 0000000..94a67e0 --- /dev/null +++ b/setup/base/__init__.py @@ -0,0 +1,6 @@ +"""Base classes for SuperClaude installation system""" + +from .component import Component +from .installer import Installer + +__all__ = ['Component', 'Installer'] \ No newline at end of file diff --git a/setup/base/component.py b/setup/base/component.py new file mode 100644 index 0000000..c0f2500 --- /dev/null +++ b/setup/base/component.py @@ -0,0 +1,361 @@ +""" +Abstract base class for installable components +""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Tuple, Optional, Any +from pathlib import Path +import json +from ..managers.file_manager import FileManager +from ..managers.settings_manager import SettingsManager +from ..utils.logger import get_logger +from ..utils.security import SecurityValidator + + +class Component(ABC): + """Base class for all installable components""" + + def __init__(self, install_dir: Optional[Path] = None, component_subdir: Path = Path('')): + """ + Initialize component with installation directory + + Args: + install_dir: Target installation directory (defaults to ~/.claude) + """ + from .. import DEFAULT_INSTALL_DIR + self.install_dir = install_dir or DEFAULT_INSTALL_DIR + self.settings_manager = SettingsManager(self.install_dir) + self.logger = get_logger() + self.component_files = self._discover_component_files() + self.file_manager = FileManager() + self.install_component_subdir = self.install_dir / component_subdir + + @abstractmethod + def get_metadata(self) -> Dict[str, str]: + """ + Return component metadata + + Returns: + Dict containing: + - name: Component name + - version: Component version + - description: Component description + - category: Component category (core, command, integration, etc.) + """ + pass + + def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]: + """ + Check prerequisites for this component + + Returns: + Tuple of (success: bool, error_messages: List[str]) + """ + errors = [] + + # Check if we have read access to source files + source_dir = self._get_source_dir() + if not source_dir or (source_dir and not source_dir.exists()): + errors.append(f"Source directory not found: {source_dir}") + return False, errors + + # Check if all required framework files exist + missing_files = [] + for filename in self.component_files: + source_file = source_dir / filename + if not source_file.exists(): + missing_files.append(filename) + + if missing_files: + errors.append(f"Missing component files: {missing_files}") + + # Check write permissions to install directory + has_perms, missing = SecurityValidator.check_permissions( + self.install_dir, {'write'} + ) + if not has_perms: + errors.append(f"No write permissions to {self.install_dir}: {missing}") + + # Validate installation target + is_safe, validation_errors = SecurityValidator.validate_installation_target(self.install_component_subdir) + if not is_safe: + errors.extend(validation_errors) + + # Get files to install + files_to_install = self.get_files_to_install() + + # Validate all files for security + is_safe, security_errors = SecurityValidator.validate_component_files( + files_to_install, source_dir, self.install_component_subdir + ) + if not is_safe: + errors.extend(security_errors) + + if not self.file_manager.ensure_directory(self.install_component_subdir): + errors.append(f"Could not create install directory: {self.install_component_subdir}") + + return len(errors) == 0, errors + + def get_files_to_install(self) -> List[Tuple[Path, Path]]: + """ + Return list of files to install + + Returns: + List of tuples (source_path, target_path) + """ + source_dir = self._get_source_dir() + files = [] + + if source_dir: + for filename in self.component_files: + source = source_dir / filename + target = self.install_component_subdir / filename + files.append((source, target)) + + return files + + def get_settings_modifications(self) -> Dict[str, Any]: + """ + Return settings.json modifications to apply + (now only Claude Code compatible settings) + + Returns: + Dict of settings to merge into settings.json + """ + # Return empty dict as we don't modify Claude Code settings + return {} + + def install(self, config: Dict[str, Any]) -> bool: + try: + return self._install(config) + except Exception as e: + self.logger.exception(f"Unexpected error during {repr(self)} installation: {e}") + return False + + @abstractmethod + def _install(self, config: Dict[str, Any]) -> bool: + """ + Perform component-specific installation logic + + Args: + config: Installation configuration + + Returns: + True if successful, False otherwise + """ + # Validate installation + success, errors = self.validate_prerequisites() + if not success: + for error in errors: + self.logger.error(error) + return False + + # Get files to install + files_to_install = self.get_files_to_install() + + # Copy framework files + success_count = 0 + for source, target in files_to_install: + self.logger.debug(f"Copying {source.name} to {target}") + + if self.file_manager.copy_file(source, target): + success_count += 1 + self.logger.debug(f"Successfully copied {source.name}") + else: + self.logger.error(f"Failed to copy {source.name}") + + if success_count != len(files_to_install): + self.logger.error(f"Only {success_count}/{len(files_to_install)} files copied successfully") + return False + + self.logger.success(f"{repr(self)} component installed successfully ({success_count} files)") + + return self._post_install() + + + @abstractmethod + def _post_install(self) -> bool: + pass + + + @abstractmethod + def uninstall(self) -> bool: + """ + Remove component + + Returns: + True if successful, False otherwise + """ + pass + + @abstractmethod + def get_dependencies(self) -> List[str]: + """ + Return list of component dependencies + + Returns: + List of component names this component depends on + """ + pass + + @abstractmethod + def _get_source_dir(self) -> Optional[Path]: + """Get source directory for component files""" + pass + + def update(self, config: Dict[str, Any]) -> bool: + """ + Update component (default: uninstall then install) + + Args: + config: Installation configuration + + Returns: + True if successful, False otherwise + """ + # Default implementation: uninstall and reinstall + if self.uninstall(): + return self.install(config) + return False + + def get_installed_version(self) -> Optional[str]: + """ + Get currently installed version of component + + Returns: + Version string if installed, None otherwise + """ + print("GETTING INSTALLED VERSION") + settings_file = self.install_dir / "settings.json" + if settings_file.exists(): + print("SETTINGS.JSON EXISTS") + try: + with open(settings_file, 'r') as f: + settings = json.load(f) + component_name = self.get_metadata()['name'] + return settings.get('components', {}).get(component_name, {}).get('version') + except Exception: + pass + print("SETTINGS.JSON DOESNT EXIST RETURNING NONE") + return None + + def is_installed(self) -> bool: + """ + Check if component is installed + + Returns: + True if installed, False otherwise + """ + return self.get_installed_version() is not None + + def validate_installation(self) -> Tuple[bool, List[str]]: + """ + Validate that component is correctly installed + + Returns: + Tuple of (success: bool, error_messages: List[str]) + """ + errors = [] + + # Check if all files exist + for _, target in self.get_files_to_install(): + if not target.exists(): + errors.append(f"Missing file: {target}") + + # Check version in settings + if not self.get_installed_version(): + errors.append("Component not registered in settings.json") + + return len(errors) == 0, errors + + def get_size_estimate(self) -> int: + """ + Estimate installed size in bytes + + Returns: + Estimated size in bytes + """ + total_size = 0 + for source, _ in self.get_files_to_install(): + if source.exists(): + if source.is_file(): + total_size += source.stat().st_size + elif source.is_dir(): + total_size += sum(f.stat().st_size for f in source.rglob('*') if f.is_file()) + return total_size + + def _discover_component_files(self) -> List[str]: + """ + Dynamically discover framework .md files in the Core directory + + Returns: + List of framework filenames (e.g., ['CLAUDE.md', 'COMMANDS.md', ...]) + """ + source_dir = self._get_source_dir() + + if not source_dir: + return [] + + return self._discover_files_in_directory( + source_dir, + extension='.md', + exclude_patterns=['README.md', 'CHANGELOG.md', 'LICENSE.md'] + ) + + def _discover_files_in_directory(self, directory: Path, extension: str = '.md', + exclude_patterns: Optional[List[str]] = None) -> List[str]: + """ + Shared utility for discovering files in a directory + + Args: + directory: Directory to scan + extension: File extension to look for (default: '.md') + exclude_patterns: List of filename patterns to exclude + + Returns: + List of filenames found in the directory + """ + if exclude_patterns is None: + exclude_patterns = [] + + try: + if not directory.exists(): + self.logger.warning(f"Source directory not found: {directory}") + return [] + + if not directory.is_dir(): + self.logger.warning(f"Source path is not a directory: {directory}") + return [] + + # Discover files with the specified extension + files = [] + for file_path in directory.iterdir(): + if (file_path.is_file() and + file_path.suffix.lower() == extension.lower() and + file_path.name not in exclude_patterns): + files.append(file_path.name) + + # Sort for consistent ordering + files.sort() + + self.logger.debug(f"Discovered {len(files)} {extension} files in {directory}") + if files: + self.logger.debug(f"Files found: {files}") + + return files + + except PermissionError: + self.logger.error(f"Permission denied accessing directory: {directory}") + return [] + except Exception as e: + self.logger.error(f"Error discovering files in {directory}: {e}") + return [] + + def __str__(self) -> str: + """String representation of component""" + metadata = self.get_metadata() + return f"{metadata['name']} v{metadata['version']}" + + def __repr__(self) -> str: + """Developer representation of component""" + return f"<{self.__class__.__name__}({self.get_metadata()['name']})>" diff --git a/setup/base/installer.py b/setup/base/installer.py new file mode 100644 index 0000000..7f3eb99 --- /dev/null +++ b/setup/base/installer.py @@ -0,0 +1,331 @@ +""" +Base installer logic for SuperClaude installation system fixed some issues +""" + +from typing import List, Dict, Optional, Set, Tuple, Any +from pathlib import Path +import shutil +import tempfile +from datetime import datetime +from .component import Component + + +class Installer: + """Main installer orchestrator""" + + def __init__(self, + install_dir: Optional[Path] = None, + dry_run: bool = False): + """ + Initialize installer + + Args: + install_dir: Target installation directory + dry_run: If True, only simulate installation + """ + from .. import DEFAULT_INSTALL_DIR + self.install_dir = install_dir or DEFAULT_INSTALL_DIR + self.dry_run = dry_run + self.components: Dict[str, Component] = {} + self.installed_components: Set[str] = set() + self.updated_components: Set[str] = set() + + self.failed_components: Set[str] = set() + self.skipped_components: Set[str] = set() + self.backup_path: Optional[Path] = None + + def register_component(self, component: Component) -> None: + """ + Register a component for installation + + Args: + component: Component instance to register + """ + metadata = component.get_metadata() + self.components[metadata['name']] = component + + def register_components(self, components: List[Component]) -> None: + """ + Register multiple components + + Args: + components: List of component instances + """ + for component in components: + self.register_component(component) + + def resolve_dependencies(self, component_names: List[str]) -> List[str]: + """ + Resolve component dependencies in correct installation order + + Args: + component_names: List of component names to install + + Returns: + Ordered list of component names including dependencies + + Raises: + ValueError: If circular dependencies detected or unknown component + """ + resolved = [] + resolving = set() + + def resolve(name: str): + if name in resolved: + return + + if name in resolving: + raise ValueError( + f"Circular dependency detected involving {name}") + + if name not in self.components: + raise ValueError(f"Unknown component: {name}") + + resolving.add(name) + + # Resolve dependencies first + for dep in self.components[name].get_dependencies(): + resolve(dep) + + resolving.remove(name) + resolved.append(name) + + # Resolve each requested component + for name in component_names: + resolve(name) + + return resolved + + def validate_system_requirements(self) -> Tuple[bool, List[str]]: + """ + Validate system requirements for all registered components + + Returns: + Tuple of (success: bool, error_messages: List[str]) + """ + errors = [] + + # Check disk space (500MB minimum) + try: + stat = shutil.disk_usage(self.install_dir.parent) + free_mb = stat.free / (1024 * 1024) + if free_mb < 500: + errors.append( + f"Insufficient disk space: {free_mb:.1f}MB free (500MB required)" + ) + except Exception as e: + errors.append(f"Could not check disk space: {e}") + + # Check write permissions + test_file = self.install_dir / ".write_test" + try: + self.install_dir.mkdir(parents=True, exist_ok=True) + test_file.touch() + test_file.unlink() + except Exception as e: + errors.append(f"No write permission to {self.install_dir}: {e}") + + return len(errors) == 0, errors + + def create_backup(self) -> Optional[Path]: + """ + Create backup of existing installation + + Returns: + Path to backup archive or None if no existing installation + """ + if not self.install_dir.exists(): + return None + + if self.dry_run: + return self.install_dir / "backup_dryrun.tar.gz" + + # Create backup directory + backup_dir = self.install_dir / "backups" + backup_dir.mkdir(exist_ok=True) + + # Create timestamped backup + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_name = f"superclaude_backup_{timestamp}" + backup_path = backup_dir / f"{backup_name}.tar.gz" + + # Create temporary directory for backup + with tempfile.TemporaryDirectory() as temp_dir: + temp_backup = Path(temp_dir) / backup_name + + # Ensure temp backup directory exists + temp_backup.mkdir(parents=True, exist_ok=True) + + # Copy all files except backups directory + for item in self.install_dir.iterdir(): + if item.name != "backups": + try: + if item.is_file(): + shutil.copy2(item, temp_backup / item.name) + elif item.is_dir(): + shutil.copytree(item, temp_backup / item.name) + except Exception as e: + # Log warning but continue backup process + print(f"Warning: Could not backup {item.name}: {e}") + + # Create archive only if there are files to backup + if any(temp_backup.iterdir()): + shutil.make_archive(backup_path.with_suffix(''), 'gztar', + temp_dir, backup_name) + else: + # Create empty backup file to indicate backup was attempted + backup_path.touch() + print( + f"Warning: No files to backup, created empty backup marker: {backup_path.name}" + ) + + self.backup_path = backup_path + return backup_path + + def install_component(self, component_name: str, + config: Dict[str, Any]) -> bool: + """ + Install a single component + + Args: + component_name: Name of component to install + config: Installation configuration + + Returns: + True if successful, False otherwise + """ + if component_name not in self.components: + raise ValueError(f"Unknown component: {component_name}") + + component = self.components[component_name] + + # Skip if already installed + if component_name in self.installed_components: + return True + + # Check prerequisites + success, errors = component.validate_prerequisites() + if not success: + print(f"Prerequisites failed for {component_name}:") + for error in errors: + print(f" - {error}") + self.failed_components.add(component_name) + return False + + # Perform installation + try: + if self.dry_run: + print(f"[DRY RUN] Would install {component_name}") + success = True + else: + success = component.install(config) + + if success: + self.installed_components.add(component_name) + self.updated_components.add(component_name) + else: + self.failed_components.add(component_name) + + return success + + except Exception as e: + print(f"Error installing {component_name}: {e}") + self.failed_components.add(component_name) + return False + + def install_components(self, + component_names: List[str], + config: Optional[Dict[str, Any]] = None) -> bool: + """ + Install multiple components in dependency order + + Args: + component_names: List of component names to install + config: Installation configuration + + Returns: + True if all successful, False if any failed + """ + config = config or {} + + # Resolve dependencies + try: + ordered_names = self.resolve_dependencies(component_names) + except ValueError as e: + print(f"Dependency resolution error: {e}") + return False + + # Validate system requirements + success, errors = self.validate_system_requirements() + if not success: + print("System requirements not met:") + for error in errors: + print(f" - {error}") + return False + + # Create backup if updating + if self.install_dir.exists() and not self.dry_run: + print("Creating backup of existing installation...") + self.create_backup() + + # Install each component + all_success = True + for name in ordered_names: + print(f"\nInstalling {name}...") + if not self.install_component(name, config): + all_success = False + # Continue installing other components even if one fails + + if not self.dry_run: + self._run_post_install_validation() + + return all_success + + def _run_post_install_validation(self) -> None: + """Run post-installation validation for all installed components""" + print("\nRunning post-installation validation...") + + all_valid = True + for name in self.installed_components: + component = self.components[name] + success, errors = component.validate_installation() + + if success: + print(f" ✓ {name}: Valid") + else: + print(f" ✗ {name}: Invalid") + for error in errors: + print(f" - {error}") + all_valid = False + + if all_valid: + print("\nAll components validated successfully!") + else: + print("\nSome components failed validation. Check errors above.") + def update_components(self, component_names: List[str], config: Dict[str, Any]) -> bool: + """Alias for update operation (uses install logic)""" + return self.install_components(component_names, config) + + + def get_installation_summary(self) -> Dict[str, Any]: + """ + Get summary of installation results + + Returns: + Dict with installation statistics and results + """ + return { + 'installed': list(self.installed_components), + 'failed': list(self.failed_components), + 'skipped': list(self.skipped_components), + 'backup_path': str(self.backup_path) if self.backup_path else None, + 'install_dir': str(self.install_dir), + 'dry_run': self.dry_run + } + + def get_update_summary(self) -> Dict[str, Any]: + return { + 'updated': list(self.updated_components), + 'failed': list(self.failed_components), + 'backup_path': str(self.backup_path) if self.backup_path else None + } diff --git a/setup/components/__init__.py b/setup/components/__init__.py new file mode 100644 index 0000000..20f6a82 --- /dev/null +++ b/setup/components/__init__.py @@ -0,0 +1,13 @@ +"""Component implementations for SuperClaude installation system""" + +from .core import CoreComponent +from .commands import CommandsComponent +from .mcp import MCPComponent +from .hooks import HooksComponent + +__all__ = [ + 'CoreComponent', + 'CommandsComponent', + 'MCPComponent', + 'HooksComponent' +] \ No newline at end of file diff --git a/setup/components/commands.py b/setup/components/commands.py new file mode 100644 index 0000000..c7983f8 --- /dev/null +++ b/setup/components/commands.py @@ -0,0 +1,329 @@ +""" +Commands component for SuperClaude slash command definitions +""" + +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path + +from ..base.component import Component + +class CommandsComponent(Component): + """SuperClaude slash commands component""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize commands component""" + super().__init__(install_dir, Path("commands/sc")) + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "commands", + "version": "3.0.0", + "description": "SuperClaude slash command definitions", + "category": "commands" + } + + def get_metadata_modifications(self) -> Dict[str, Any]: + """Get metadata modifications for commands component""" + return { + "components": { + "commands": { + "version": "3.0.0", + "installed": True, + "files_count": len(self.component_files) + } + }, + "commands": { + "enabled": True, + "version": "3.0.0", + "auto_update": False + } + } + + def _install(self, config: Dict[str, Any]) -> bool: + """Install commands component""" + self.logger.info("Installing SuperClaude command definitions...") + + # Check for and migrate existing commands from old location + self._migrate_existing_commands() + + return super()._install(config); + + def _post_install(self): + # Update metadata + try: + metadata_mods = self.get_metadata_modifications() + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with commands configuration") + + # Add component registration to metadata + self.settings_manager.add_component_registration("commands", { + "version": "3.0.0", + "category": "commands", + "files_count": len(self.component_files) + }) + self.logger.info("Updated metadata with commands component registration") + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + return True + + def uninstall(self) -> bool: + """Uninstall commands component""" + try: + self.logger.info("Uninstalling SuperClaude commands component...") + + # Remove command files from sc subdirectory + commands_dir = self.install_dir / "commands" / "sc" + removed_count = 0 + + for filename in self.component_files: + file_path = commands_dir / filename + if self.file_manager.remove_file(file_path): + removed_count += 1 + self.logger.debug(f"Removed {filename}") + else: + self.logger.warning(f"Could not remove {filename}") + + # Also check and remove any old commands in root commands directory + old_commands_dir = self.install_dir / "commands" + old_removed_count = 0 + + for filename in self.component_files: + old_file_path = old_commands_dir / filename + if old_file_path.exists() and old_file_path.is_file(): + if self.file_manager.remove_file(old_file_path): + old_removed_count += 1 + self.logger.debug(f"Removed old {filename}") + else: + self.logger.warning(f"Could not remove old {filename}") + + if old_removed_count > 0: + self.logger.info(f"Also removed {old_removed_count} commands from old location") + + removed_count += old_removed_count + + # Remove sc subdirectory if empty + try: + if commands_dir.exists(): + remaining_files = list(commands_dir.iterdir()) + if not remaining_files: + commands_dir.rmdir() + self.logger.debug("Removed empty sc commands directory") + + # Also remove parent commands directory if empty + parent_commands_dir = self.install_dir / "commands" + if parent_commands_dir.exists(): + remaining_files = list(parent_commands_dir.iterdir()) + if not remaining_files: + parent_commands_dir.rmdir() + self.logger.debug("Removed empty parent commands directory") + except Exception as e: + self.logger.warning(f"Could not remove commands directory: {e}") + + # Update metadata to remove commands component + try: + if self.settings_manager.is_component_installed("commands"): + self.settings_manager.remove_component_registration("commands") + # Also remove commands configuration from metadata + metadata = self.settings_manager.load_metadata() + if "commands" in metadata: + del metadata["commands"] + self.settings_manager.save_metadata(metadata) + self.logger.info("Removed commands component from metadata") + except Exception as e: + self.logger.warning(f"Could not update metadata: {e}") + + self.logger.success(f"Commands component uninstalled ({removed_count} files removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during commands uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get dependencies""" + return ["core"] + + def update(self, config: Dict[str, Any]) -> bool: + """Update commands component""" + try: + self.logger.info("Updating SuperClaude commands component...") + + # Check current version + current_version = self.settings_manager.get_component_version("commands") + target_version = self.get_metadata()["version"] + + if current_version == target_version: + self.logger.info(f"Commands component already at version {target_version}") + return True + + self.logger.info(f"Updating commands component from {current_version} to {target_version}") + + # Create backup of existing command files + commands_dir = self.install_dir / "commands" / "sc" + backup_files = [] + + if commands_dir.exists(): + for filename in self.component_files: + file_path = commands_dir / filename + if file_path.exists(): + backup_path = self.file_manager.backup_file(file_path) + if backup_path: + backup_files.append(backup_path) + self.logger.debug(f"Backed up {filename}") + + # Perform installation (overwrites existing files) + success = self.install(config) + + if success: + # Remove backup files on successful update + for backup_path in backup_files: + try: + backup_path.unlink() + except Exception: + pass # Ignore cleanup errors + + self.logger.success(f"Commands component updated to version {target_version}") + else: + # Restore from backup on failure + self.logger.warning("Update failed, restoring from backup...") + for backup_path in backup_files: + try: + original_path = backup_path.with_suffix('') + backup_path.rename(original_path) + self.logger.debug(f"Restored {original_path.name}") + except Exception as e: + self.logger.error(f"Could not restore {backup_path}: {e}") + + return success + + except Exception as e: + self.logger.exception(f"Unexpected error during commands update: {e}") + return False + + def validate_installation(self) -> Tuple[bool, List[str]]: + """Validate commands component installation""" + errors = [] + + # Check if sc commands directory exists + commands_dir = self.install_dir / "commands" / "sc" + if not commands_dir.exists(): + errors.append("SC commands directory not found") + return False, errors + + # Check if all command files exist + for filename in self.component_files: + file_path = commands_dir / filename + if not file_path.exists(): + errors.append(f"Missing command file: {filename}") + elif not file_path.is_file(): + errors.append(f"Command file is not a regular file: {filename}") + + # Check metadata registration + if not self.settings_manager.is_component_installed("commands"): + errors.append("Commands component not registered in metadata") + else: + # Check version matches + installed_version = self.settings_manager.get_component_version("commands") + expected_version = self.get_metadata()["version"] + if installed_version != expected_version: + errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") + + return len(errors) == 0, errors + + def _get_source_dir(self) -> Path: + """Get source directory for command files""" + # Assume we're in SuperClaude/setup/components/commands.py + # and command files are in SuperClaude/SuperClaude/Commands/ + project_root = Path(__file__).parent.parent.parent + return project_root / "SuperClaude" / "Commands" + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + total_size = 0 + source_dir = self._get_source_dir() + + for filename in self.component_files: + file_path = source_dir / filename + if file_path.exists(): + total_size += file_path.stat().st_size + + # Add overhead for directory and settings + total_size += 5120 # ~5KB overhead + + return total_size + + def get_installation_summary(self) -> Dict[str, Any]: + """Get installation summary""" + return { + "component": self.get_metadata()["name"], + "version": self.get_metadata()["version"], + "files_installed": len(self.component_files), + "command_files": self.component_files, + "estimated_size": self.get_size_estimate(), + "install_directory": str(self.install_dir / "commands" / "sc"), + "dependencies": self.get_dependencies() + } + + def _migrate_existing_commands(self) -> None: + """Migrate existing commands from old location to new sc subdirectory""" + try: + old_commands_dir = self.install_dir / "commands" + new_commands_dir = self.install_dir / "commands" / "sc" + + # Check if old commands exist in root commands directory + migrated_count = 0 + commands_to_migrate = [] + + if old_commands_dir.exists(): + for filename in self.component_files: + old_file_path = old_commands_dir / filename + if old_file_path.exists() and old_file_path.is_file(): + commands_to_migrate.append(filename) + + if commands_to_migrate: + self.logger.info(f"Found {len(commands_to_migrate)} existing commands to migrate to sc/ subdirectory") + + # Ensure new directory exists + if not self.file_manager.ensure_directory(new_commands_dir): + self.logger.error(f"Could not create sc commands directory: {new_commands_dir}") + return + + # Move files from old to new location + for filename in commands_to_migrate: + old_file_path = old_commands_dir / filename + new_file_path = new_commands_dir / filename + + try: + # Copy file to new location + if self.file_manager.copy_file(old_file_path, new_file_path): + # Remove old file + if self.file_manager.remove_file(old_file_path): + migrated_count += 1 + self.logger.debug(f"Migrated {filename} to sc/ subdirectory") + else: + self.logger.warning(f"Could not remove old {filename}") + else: + self.logger.warning(f"Could not copy {filename} to sc/ subdirectory") + except Exception as e: + self.logger.warning(f"Error migrating {filename}: {e}") + + if migrated_count > 0: + self.logger.success(f"Successfully migrated {migrated_count} commands to /sc: namespace") + self.logger.info("Commands are now available as /sc:analyze, /sc:build, etc.") + + # Try to remove old commands directory if empty + try: + if old_commands_dir.exists(): + remaining_files = [f for f in old_commands_dir.iterdir() if f.is_file()] + if not remaining_files: + # Only remove if no user files remain + old_commands_dir.rmdir() + self.logger.debug("Removed empty old commands directory") + except Exception as e: + self.logger.debug(f"Could not remove old commands directory: {e}") + + except Exception as e: + self.logger.warning(f"Error during command migration: {e}") diff --git a/setup/components/core.py b/setup/components/core.py new file mode 100644 index 0000000..eec433c --- /dev/null +++ b/setup/components/core.py @@ -0,0 +1,248 @@ +""" +Core component for SuperClaude framework files installation +""" + +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path +import shutil + +from ..base.component import Component + +class CoreComponent(Component): + """Core SuperClaude framework files component""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize core component""" + super().__init__(install_dir) + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "core", + "version": "3.0.0", + "description": "SuperClaude framework documentation and core files", + "category": "core" + } + + def get_metadata_modifications(self) -> Dict[str, Any]: + """Get metadata modifications for SuperClaude""" + return { + "framework": { + "version": "3.0.0", + "name": "SuperClaude", + "description": "AI-enhanced development framework for Claude Code", + "installation_type": "global", + "components": ["core"] + }, + "superclaude": { + "enabled": True, + "version": "3.0.0", + "profile": "default", + "auto_update": False + } + } + + def _install(self, config: Dict[str, Any]) -> bool: + """Install core component""" + self.logger.info("Installing SuperClaude core framework files...") + + return super()._install(config); + + def _post_install(self): + # Create or update metadata + try: + metadata_mods = self.get_metadata_modifications() + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with framework configuration") + + # Add component registration to metadata + self.settings_manager.add_component_registration("core", { + "version": "3.0.0", + "category": "core", + "files_count": len(self.component_files) + }) + + self.logger.info("Updated metadata with core component registration") + + # Migrate any existing SuperClaude data from settings.json + if self.settings_manager.migrate_superclaude_data(): + self.logger.info("Migrated existing SuperClaude data from settings.json") + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + # Create additional directories for other components + additional_dirs = ["commands", "hooks", "backups", "logs"] + for dirname in additional_dirs: + dir_path = self.install_dir / dirname + if not self.file_manager.ensure_directory(dir_path): + self.logger.warning(f"Could not create directory: {dir_path}") + + return True + + + def uninstall(self) -> bool: + """Uninstall core component""" + try: + self.logger.info("Uninstalling SuperClaude core component...") + + # Remove framework files + removed_count = 0 + for filename in self.component_files: + file_path = self.install_dir / filename + if self.file_manager.remove_file(file_path): + removed_count += 1 + self.logger.debug(f"Removed {filename}") + else: + self.logger.warning(f"Could not remove {filename}") + + # Update metadata to remove core component + try: + if self.settings_manager.is_component_installed("core"): + self.settings_manager.remove_component_registration("core") + metadata_mods = self.get_metadata_modifications() + metadata = self.settings_manager.load_metadata() + for key in metadata_mods.keys(): + if key in metadata: + del metadata[key] + + self.settings_manager.save_metadata(metadata) + self.logger.info("Removed core component from metadata") + except Exception as e: + self.logger.warning(f"Could not update metadata: {e}") + + self.logger.success(f"Core component uninstalled ({removed_count} files removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during core uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get component dependencies (core has none)""" + return [] + + def update(self, config: Dict[str, Any]) -> bool: + """Update core component""" + try: + self.logger.info("Updating SuperClaude core component...") + + # Check current version + current_version = self.settings_manager.get_component_version("core") + target_version = self.get_metadata()["version"] + + if current_version == target_version: + self.logger.info(f"Core component already at version {target_version}") + return True + + self.logger.info(f"Updating core component from {current_version} to {target_version}") + + # Create backup of existing files + backup_files = [] + for filename in self.component_files: + file_path = self.install_dir / filename + if file_path.exists(): + backup_path = self.file_manager.backup_file(file_path) + if backup_path: + backup_files.append(backup_path) + self.logger.debug(f"Backed up {filename}") + + # Perform installation (overwrites existing files) + success = self.install(config) + + if success: + # Remove backup files on successful update + for backup_path in backup_files: + try: + backup_path.unlink() + except Exception: + pass # Ignore cleanup errors + + self.logger.success(f"Core component updated to version {target_version}") + else: + # Restore from backup on failure + self.logger.warning("Update failed, restoring from backup...") + for backup_path in backup_files: + try: + original_path = backup_path.with_suffix('') + shutil.move(str(backup_path), str(original_path)) + self.logger.debug(f"Restored {original_path.name}") + except Exception as e: + self.logger.error(f"Could not restore {backup_path}: {e}") + + return success + + except Exception as e: + self.logger.exception(f"Unexpected error during core update: {e}") + return False + + def validate_installation(self) -> Tuple[bool, List[str]]: + """Validate core component installation""" + errors = [] + + # Check if all framework files exist + for filename in self.component_files: + file_path = self.install_dir / filename + if not file_path.exists(): + errors.append(f"Missing framework file: {filename}") + elif not file_path.is_file(): + errors.append(f"Framework file is not a regular file: {filename}") + + # Check metadata registration + if not self.settings_manager.is_component_installed("core"): + errors.append("Core component not registered in metadata") + else: + # Check version matches + installed_version = self.settings_manager.get_component_version("core") + expected_version = self.get_metadata()["version"] + if installed_version != expected_version: + errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") + + # Check metadata structure + try: + framework_config = self.settings_manager.get_metadata_setting("framework") + if not framework_config: + errors.append("Missing framework configuration in metadata") + else: + required_keys = ["version", "name", "description"] + for key in required_keys: + if key not in framework_config: + errors.append(f"Missing framework.{key} in metadata") + except Exception as e: + errors.append(f"Could not validate metadata: {e}") + + return len(errors) == 0, errors + + def _get_source_dir(self): + """Get source directory for framework files""" + # Assume we're in SuperClaude/setup/components/core.py + # and framework files are in SuperClaude/SuperClaude/Core/ + project_root = Path(__file__).parent.parent.parent + return project_root / "SuperClaude" / "Core" + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + total_size = 0 + source_dir = self._get_source_dir() + + for filename in self.component_files: + file_path = source_dir / filename + if file_path.exists(): + total_size += file_path.stat().st_size + + # Add overhead for settings.json and directories + total_size += 10240 # ~10KB overhead + + return total_size + + def get_installation_summary(self) -> Dict[str, Any]: + """Get installation summary""" + return { + "component": self.get_metadata()["name"], + "version": self.get_metadata()["version"], + "files_installed": len(self.component_files), + "framework_files": self.component_files, + "estimated_size": self.get_size_estimate(), + "install_directory": str(self.install_dir), + "dependencies": self.get_dependencies() + } diff --git a/setup/components/hooks.py b/setup/components/hooks.py new file mode 100644 index 0000000..c0e9b08 --- /dev/null +++ b/setup/components/hooks.py @@ -0,0 +1,366 @@ +""" +Hooks component for Claude Code hooks integration (future-ready) +""" + +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path + +from ..base.component import Component + + +class HooksComponent(Component): + """Claude Code hooks integration component""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize hooks component""" + super().__init__(install_dir, Path("hooks")) + + # Define hook files to install (when hooks are ready) + self.hook_files = [ + "pre_tool_use.py", + "post_tool_use.py", + "error_handler.py", + "context_accumulator.py", + "performance_monitor.py" + ] + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "hooks", + "version": "3.0.0", + "description": "Claude Code hooks integration (future-ready)", + "category": "integration" + } + def get_metadata_modifications(self) -> Dict[str, Any]: + # Build hooks configuration based on available files + hook_config = {} + for filename in self.hook_files: + hook_path = self.install_component_subdir / filename + if hook_path.exists(): + hook_name = filename.replace('.py', '') + hook_config[hook_name] = [str(hook_path)] + + metadata_mods = { + "components": { + "hooks": { + "version": "3.0.0", + "installed": True, + "files_count": len(hook_config) + } + } + } + + # Only add hooks configuration if we have actual hook files + if hook_config: + metadata_mods["hooks"] = { + "enabled": True, + **hook_config + } + + + return metadata_mods + + def _install(self, config: Dict[str, Any]) -> bool: + """Install hooks component""" + self.logger.info("Installing SuperClaude hooks component...") + + # This component is future-ready - hooks aren't implemented yet + source_dir = self._get_source_dir() + + if not source_dir.exists() or (source_dir / "PLACEHOLDER.py").exists : + self.logger.info("Hooks are not yet implemented - installing placeholder component") + + # Create placeholder hooks directory + if not self.file_manager.ensure_directory(self.install_component_subdir): + self.logger.error(f"Could not create hooks directory: {self.install_component_subdir}") + return False + + # Create placeholder file + placeholder_content = '''""" +SuperClaude Hooks - Future Implementation + +This directory is reserved for Claude Code hooks integration. +Hooks will provide lifecycle management and automation capabilities. + +Planned hooks: +- pre_tool_use: Execute before tool usage +- post_tool_use: Execute after tool completion +- error_handler: Handle tool errors and recovery +- context_accumulator: Manage context across operations +- performance_monitor: Track and optimize performance + +For more information, see SuperClaude documentation. +""" + +# Placeholder for future hooks implementation +def placeholder_hook(): +"""Placeholder hook function""" +pass +''' + + placeholder_path = self.install_component_subdir / "PLACEHOLDER.py" + try: + with open(placeholder_path, 'w') as f: + f.write(placeholder_content) + self.logger.debug("Created hooks placeholder file") + except Exception as e: + self.logger.warning(f"Could not create placeholder file: {e}") + + # Update settings with placeholder registration + try: + metadata_mods = { + "components": { + "hooks": { + "version": "3.0.0", + "installed": True, + "status": "placeholder", + "files_count": 0 + } + } + } + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with hooks component registration") + except Exception as e: + self.logger.error(f"Failed to update metadata for hooks component: {e}") + return False + + self.logger.success("Hooks component installed successfully (placeholder)") + return True + + # If hooks source directory exists, install actual hooks + self.logger.info("Installing actual hook files...") + + # Validate installation + success, errors = self.validate_prerequisites(Path("hooks")) + if not success: + for error in errors: + self.logger.error(error) + return False + + # Get files to install + files_to_install = self.get_files_to_install() + + if not files_to_install: + self.logger.warning("No hook files found to install") + return False + + # Copy hook files + success_count = 0 + for source, target in files_to_install: + self.logger.debug(f"Copying {source.name} to {target}") + + if self.file_manager.copy_file(source, target): + success_count += 1 + self.logger.debug(f"Successfully copied {source.name}") + else: + self.logger.error(f"Failed to copy {source.name}") + + if success_count != len(files_to_install): + self.logger.error(f"Only {success_count}/{len(files_to_install)} hook files copied successfully") + return False + + self.logger.success(f"Hooks component installed successfully ({success_count} hook files)") + + return self._post_install() + + def _post_install(self): + # Update metadata + try: + metadata_mods = self.get_metadata_modifications() + self.settings_manager.update_metadata(metadata_mods) + self.logger.info("Updated metadata with hooks configuration") + + # Add hook registration to metadata + self.settings_manager.add_component_registration("hooks", { + "version": "3.0.0", + "category": "commands", + "files_count": len(self.hook_files) + }) + + self.logger.info("Updated metadata with commands component registration") + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + return True + + def uninstall(self) -> bool: + """Uninstall hooks component""" + try: + self.logger.info("Uninstalling SuperClaude hooks component...") + + # Remove hook files and placeholder + removed_count = 0 + + # Remove actual hook files + for filename in self.hook_files: + file_path = self.install_component_subdir / filename + if self.file_manager.remove_file(file_path): + removed_count += 1 + self.logger.debug(f"Removed {filename}") + + # Remove placeholder file + placeholder_path = self.install_component_subdir / "PLACEHOLDER.py" + if self.file_manager.remove_file(placeholder_path): + removed_count += 1 + self.logger.debug("Removed hooks placeholder") + + # Remove hooks directory if empty + try: + if self.install_component_subdir.exists(): + remaining_files = list(self.install_component_subdir.iterdir()) + if not remaining_files: + self.install_component_subdir.rmdir() + self.logger.debug("Removed empty hooks directory") + except Exception as e: + self.logger.warning(f"Could not remove hooks directory: {e}") + + # Update settings.json to remove hooks component and configuration + try: + if self.settings_manager.is_component_installed("hooks"): + self.settings_manager.remove_component_registration("hooks") + + # Also remove hooks configuration section if it exists + settings = self.settings_manager.load_settings() + if "hooks" in settings: + del settings["hooks"] + self.settings_manager.save_settings(settings) + + self.logger.info("Removed hooks component and configuration from settings.json") + except Exception as e: + self.logger.warning(f"Could not update settings.json: {e}") + + self.logger.success(f"Hooks component uninstalled ({removed_count} files removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during hooks uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get dependencies""" + return ["core"] + + def update(self, config: Dict[str, Any]) -> bool: + """Update hooks component""" + try: + self.logger.info("Updating SuperClaude hooks component...") + + # Check current version + current_version = self.settings_manager.get_component_version("hooks") + target_version = self.get_metadata()["version"] + + if current_version == target_version: + self.logger.info(f"Hooks component already at version {target_version}") + return True + + self.logger.info(f"Updating hooks component from {current_version} to {target_version}") + + # Create backup of existing hook files + backup_files = [] + + if self.install_component_subdir.exists(): + for filename in self.hook_files + ["PLACEHOLDER.py"]: + file_path = self.install_component_subdir / filename + if file_path.exists(): + backup_path = self.file_manager.backup_file(file_path) + if backup_path: + backup_files.append(backup_path) + self.logger.debug(f"Backed up {filename}") + + # Perform installation (overwrites existing files) + success = self.install(config) + + if success: + # Remove backup files on successful update + for backup_path in backup_files: + try: + backup_path.unlink() + except Exception: + pass # Ignore cleanup errors + + self.logger.success(f"Hooks component updated to version {target_version}") + else: + # Restore from backup on failure + self.logger.warning("Update failed, restoring from backup...") + for backup_path in backup_files: + try: + original_path = backup_path.with_suffix('') + backup_path.rename(original_path) + self.logger.debug(f"Restored {original_path.name}") + except Exception as e: + self.logger.error(f"Could not restore {backup_path}: {e}") + + return success + + except Exception as e: + self.logger.exception(f"Unexpected error during hooks update: {e}") + return False + + def validate_installation(self) -> Tuple[bool, List[str]]: + """Validate hooks component installation""" + errors = [] + + # Check if hooks directory exists + if not self.install_component_subdir.exists(): + errors.append("Hooks directory not found") + return False, errors + + # Check settings.json registration + if not self.settings_manager.is_component_installed("hooks"): + errors.append("Hooks component not registered in settings.json") + else: + # Check version matches + installed_version = self.settings_manager.get_component_version("hooks") + expected_version = self.get_metadata()["version"] + if installed_version != expected_version: + errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") + + # Check if we have either actual hooks or placeholder + has_placeholder = (self.install_component_subdir / "PLACEHOLDER.py").exists() + has_actual_hooks = any((self.install_component_subdir / filename).exists() for filename in self.hook_files) + + if not has_placeholder and not has_actual_hooks: + errors.append("No hook files or placeholder found") + + return len(errors) == 0, errors + + def _get_source_dir(self) -> Path: + """Get source directory for hook files""" + # Assume we're in SuperClaude/setup/components/hooks.py + # and hook files are in SuperClaude/SuperClaude/Hooks/ + project_root = Path(__file__).parent.parent.parent + return project_root / "SuperClaude" / "Hooks" + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + # Estimate based on placeholder or actual files + source_dir = self._get_source_dir() + total_size = 0 + + if source_dir.exists(): + for filename in self.hook_files: + file_path = source_dir / filename + if file_path.exists(): + total_size += file_path.stat().st_size + + # Add placeholder overhead or minimum size + total_size = max(total_size, 10240) # At least 10KB + + return total_size + + def get_installation_summary(self) -> Dict[str, Any]: + """Get installation summary""" + source_dir = self._get_source_dir() + status = "placeholder" if not source_dir.exists() else "implemented" + + return { + "component": self.get_metadata()["name"], + "version": self.get_metadata()["version"], + "status": status, + "hook_files": self.hook_files if source_dir.exists() else ["PLACEHOLDER.py"], + "estimated_size": self.get_size_estimate(), + "install_directory": str(self.install_dir / "hooks"), + "dependencies": self.get_dependencies() + } diff --git a/setup/components/mcp.py b/setup/components/mcp.py new file mode 100644 index 0000000..90cb1f5 --- /dev/null +++ b/setup/components/mcp.py @@ -0,0 +1,498 @@ +""" +MCP component for MCP server integration +""" + +import subprocess +import sys +from typing import Dict, List, Tuple, Optional, Any +from pathlib import Path + +from ..base.component import Component +from ..utils.ui import display_info, display_warning + + +class MCPComponent(Component): + """MCP servers integration component""" + + def __init__(self, install_dir: Optional[Path] = None): + """Initialize MCP component""" + super().__init__(install_dir) + + # Define MCP servers to install + self.mcp_servers = { + "sequential-thinking": { + "name": "sequential-thinking", + "description": "Multi-step problem solving and systematic analysis", + "npm_package": "@modelcontextprotocol/server-sequential-thinking", + "required": True + }, + "context7": { + "name": "context7", + "description": "Official library documentation and code examples", + "npm_package": "@upstash/context7-mcp", + "required": True + }, + "magic": { + "name": "magic", + "description": "Modern UI component generation and design systems", + "npm_package": "@21st-dev/magic", + "required": False, + "api_key_env": "TWENTYFIRST_API_KEY", + "api_key_description": "21st.dev API key for UI component generation" + }, + "playwright": { + "name": "playwright", + "description": "Cross-browser E2E testing and automation", + "npm_package": "@playwright/mcp@latest", + "required": False + } + } + + def get_metadata(self) -> Dict[str, str]: + """Get component metadata""" + return { + "name": "mcp", + "version": "3.0.0", + "description": "MCP server integration (Context7, Sequential, Magic, Playwright)", + "category": "integration" + } + + def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]: + """Check prerequisites""" + errors = [] + + # Check if Node.js is available + try: + result = subprocess.run( + ["node", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("Node.js not found - required for MCP servers") + else: + version = result.stdout.strip() + self.logger.debug(f"Found Node.js {version}") + + # Check version (require 18+) + try: + version_num = int(version.lstrip('v').split('.')[0]) + if version_num < 18: + errors.append(f"Node.js version {version} found, but version 18+ required") + except: + self.logger.warning(f"Could not parse Node.js version: {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("Node.js not found - required for MCP servers") + + # Check if Claude CLI is available + try: + result = subprocess.run( + ["claude", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("Claude CLI not found - required for MCP server management") + else: + version = result.stdout.strip() + self.logger.debug(f"Found Claude CLI {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("Claude CLI not found - required for MCP server management") + + # Check if npm is available + try: + result = subprocess.run( + ["npm", "--version"], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + if result.returncode != 0: + errors.append("npm not found - required for MCP server installation") + else: + version = result.stdout.strip() + self.logger.debug(f"Found npm {version}") + except (subprocess.TimeoutExpired, FileNotFoundError): + errors.append("npm not found - required for MCP server installation") + + return len(errors) == 0, errors + + def get_files_to_install(self) -> List[Tuple[Path, Path]]: + """Get files to install (none for MCP component)""" + return [] + + def get_metadata_modifications(self) -> Dict[str, Any]: + """Get metadata modifications for MCP component""" + return { + "components": { + "mcp": { + "version": "3.0.0", + "installed": True, + "servers_count": len(self.mcp_servers) + } + }, + "mcp": { + "enabled": True, + "servers": list(self.mcp_servers.keys()), + "auto_update": False + } + } + + def _check_mcp_server_installed(self, server_name: str) -> bool: + """Check if MCP server is already installed""" + try: + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + self.logger.warning(f"Could not list MCP servers: {result.stderr}") + return False + + # Parse output to check if server is installed + output = result.stdout.lower() + return server_name.lower() in output + + except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + self.logger.warning(f"Error checking MCP server status: {e}") + return False + + def _install_mcp_server(self, server_info: Dict[str, Any], config: Dict[str, Any]) -> bool: + """Install a single MCP server""" + server_name = server_info["name"] + npm_package = server_info["npm_package"] + + command = "npx" + + try: + self.logger.info(f"Installing MCP server: {server_name}") + + # Check if already installed + if self._check_mcp_server_installed(server_name): + self.logger.info(f"MCP server {server_name} already installed") + return True + + # Handle API key requirements + if "api_key_env" in server_info: + api_key_env = server_info["api_key_env"] + api_key_desc = server_info.get("api_key_description", f"API key for {server_name}") + + if not config.get("dry_run", False): + display_info(f"MCP server '{server_name}' requires an API key") + display_info(f"Environment variable: {api_key_env}") + display_info(f"Description: {api_key_desc}") + + # Check if API key is already set + import os + if not os.getenv(api_key_env): + display_warning(f"API key {api_key_env} not found in environment") + self.logger.warning(f"Proceeding without {api_key_env} - server may not function properly") + + # Install using Claude CLI + if config.get("dry_run"): + self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command} -y {npm_package}") + return True + + self.logger.debug(f"Running: claude mcp add -s user {server_name} {command} -y {npm_package}") + + result = subprocess.run( + ["claude", "mcp", "add", "-s", "user", "--", server_name, command, "-y", npm_package], + capture_output=True, + text=True, + timeout=120, # 2 minutes timeout for installation + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.success(f"Successfully installed MCP server (user scope): {server_name}") + return True + else: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + self.logger.error(f"Failed to install MCP server {server_name}: {error_msg}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"Timeout installing MCP server {server_name}") + return False + except Exception as e: + self.logger.error(f"Error installing MCP server {server_name}: {e}") + return False + + def _uninstall_mcp_server(self, server_name: str) -> bool: + """Uninstall a single MCP server""" + try: + self.logger.info(f"Uninstalling MCP server: {server_name}") + + # Check if installed + if not self._check_mcp_server_installed(server_name): + self.logger.info(f"MCP server {server_name} not installed") + return True + + self.logger.debug(f"Running: claude mcp remove {server_name} (auto-detect scope)") + + result = subprocess.run( + ["claude", "mcp", "remove", server_name], + capture_output=True, + text=True, + timeout=60, + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.success(f"Successfully uninstalled MCP server: {server_name}") + return True + else: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + self.logger.error(f"Failed to uninstall MCP server {server_name}: {error_msg}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"Timeout uninstalling MCP server {server_name}") + return False + except Exception as e: + self.logger.error(f"Error uninstalling MCP server {server_name}: {e}") + return False + + def _install(self, config: Dict[str, Any]) -> bool: + """Install MCP component""" + self.logger.info("Installing SuperClaude MCP servers...") + + # Validate prerequisites + success, errors = self.validate_prerequisites() + if not success: + for error in errors: + self.logger.error(error) + return False + + # Install each MCP server + installed_count = 0 + failed_servers = [] + + for server_name, server_info in self.mcp_servers.items(): + if self._install_mcp_server(server_info, config): + installed_count += 1 + else: + failed_servers.append(server_name) + + # Check if this is a required server + if server_info.get("required", False): + self.logger.error(f"Required MCP server {server_name} failed to install") + return False + + # Verify installation + if not config.get("dry_run", False): + self.logger.info("Verifying MCP server installation...") + try: + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode == 0: + self.logger.debug("MCP servers list:") + for line in result.stdout.strip().split('\n'): + if line.strip(): + self.logger.debug(f" {line.strip()}") + else: + self.logger.warning("Could not verify MCP server installation") + + except Exception as e: + self.logger.warning(f"Could not verify MCP installation: {e}") + + if failed_servers: + self.logger.warning(f"Some MCP servers failed to install: {failed_servers}") + self.logger.success(f"MCP component partially installed ({installed_count} servers)") + else: + self.logger.success(f"MCP component installed successfully ({installed_count} servers)") + + return self._post_install() + + def _post_install(self) -> bool: + # Update metadata + try: + metadata_mods = self.get_metadata_modifications() + self.settings_manager.update_metadata(metadata_mods) + + # Add component registration to metadata + self.settings_manager.add_component_registration("mcp", { + "version": "3.0.0", + "category": "integration", + "servers_count": len(self.mcp_servers) + }) + + self.logger.info("Updated metadata with MCP component registration") + except Exception as e: + self.logger.error(f"Failed to update metadata: {e}") + return False + + return True + + + def uninstall(self) -> bool: + """Uninstall MCP component""" + try: + self.logger.info("Uninstalling SuperClaude MCP servers...") + + # Uninstall each MCP server + uninstalled_count = 0 + + for server_name in self.mcp_servers.keys(): + if self._uninstall_mcp_server(server_name): + uninstalled_count += 1 + + # Update metadata to remove MCP component + try: + if self.settings_manager.is_component_installed("mcp"): + self.settings_manager.remove_component_registration("mcp") + # Also remove MCP configuration from metadata + metadata = self.settings_manager.load_metadata() + if "mcp" in metadata: + del metadata["mcp"] + self.settings_manager.save_metadata(metadata) + self.logger.info("Removed MCP component from metadata") + except Exception as e: + self.logger.warning(f"Could not update metadata: {e}") + + self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during MCP uninstallation: {e}") + return False + + def get_dependencies(self) -> List[str]: + """Get dependencies""" + return ["core"] + + def update(self, config: Dict[str, Any]) -> bool: + """Update MCP component""" + try: + self.logger.info("Updating SuperClaude MCP servers...") + + # Check current version + current_version = self.settings_manager.get_component_version("mcp") + target_version = self.get_metadata()["version"] + + if current_version == target_version: + self.logger.info(f"MCP component already at version {target_version}") + return True + + self.logger.info(f"Updating MCP component from {current_version} to {target_version}") + + # For MCP servers, update means reinstall to get latest versions + updated_count = 0 + failed_servers = [] + + for server_name, server_info in self.mcp_servers.items(): + try: + # Uninstall old version + if self._check_mcp_server_installed(server_name): + self._uninstall_mcp_server(server_name) + + # Install new version + if self._install_mcp_server(server_info, config): + updated_count += 1 + else: + failed_servers.append(server_name) + + except Exception as e: + self.logger.error(f"Error updating MCP server {server_name}: {e}") + failed_servers.append(server_name) + + # Update metadata + try: + # Update component version in metadata + metadata = self.settings_manager.load_metadata() + if "components" in metadata and "mcp" in metadata["components"]: + metadata["components"]["mcp"]["version"] = target_version + metadata["components"]["mcp"]["servers_count"] = len(self.mcp_servers) + if "mcp" in metadata: + metadata["mcp"]["servers"] = list(self.mcp_servers.keys()) + self.settings_manager.save_metadata(metadata) + except Exception as e: + self.logger.warning(f"Could not update metadata: {e}") + + if failed_servers: + self.logger.warning(f"Some MCP servers failed to update: {failed_servers}") + return False + else: + self.logger.success(f"MCP component updated to version {target_version}") + return True + + except Exception as e: + self.logger.exception(f"Unexpected error during MCP update: {e}") + return False + + def validate_installation(self) -> Tuple[bool, List[str]]: + """Validate MCP component installation""" + errors = [] + + # Check metadata registration + if not self.settings_manager.is_component_installed("mcp"): + errors.append("MCP component not registered in metadata") + return False, errors + + # Check version matches + installed_version = self.settings_manager.get_component_version("mcp") + expected_version = self.get_metadata()["version"] + if installed_version != expected_version: + errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") + + # Check if Claude CLI is available + try: + result = subprocess.run( + ["claude", "mcp", "list"], + capture_output=True, + text=True, + timeout=15, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + errors.append("Could not communicate with Claude CLI for MCP server verification") + else: + # Check if required servers are installed + output = result.stdout.lower() + for server_name, server_info in self.mcp_servers.items(): + if server_info.get("required", False): + if server_name.lower() not in output: + errors.append(f"Required MCP server not found: {server_name}") + + except Exception as e: + errors.append(f"Could not verify MCP server installation: {e}") + + return len(errors) == 0, errors + + def _get_source_dir(self): + """Get source directory for framework files""" + return None + + def get_size_estimate(self) -> int: + """Get estimated installation size""" + # MCP servers are installed via npm, estimate based on typical sizes + base_size = 50 * 1024 * 1024 # ~50MB for all servers combined + return base_size + + def get_installation_summary(self) -> Dict[str, Any]: + """Get installation summary""" + return { + "component": self.get_metadata()["name"], + "version": self.get_metadata()["version"], + "servers_count": len(self.mcp_servers), + "mcp_servers": list(self.mcp_servers.keys()), + "estimated_size": self.get_size_estimate(), + "dependencies": self.get_dependencies(), + "required_tools": ["node", "npm", "claude"] + } diff --git a/setup/core/__init__.py b/setup/core/__init__.py new file mode 100644 index 0000000..e330c35 --- /dev/null +++ b/setup/core/__init__.py @@ -0,0 +1,9 @@ +"""Core modules for SuperClaude installation system""" + +from .validator import Validator +from .registry import ComponentRegistry + +__all__ = [ + 'Validator', + 'ComponentRegistry' +] diff --git a/setup/core/registry.py b/setup/core/registry.py new file mode 100644 index 0000000..d9d573e --- /dev/null +++ b/setup/core/registry.py @@ -0,0 +1,395 @@ +""" +Component registry for auto-discovery and dependency resolution +""" + +import importlib +import inspect +from typing import Dict, List, Set, Optional, Type +from pathlib import Path +from ..base.component import Component + + +class ComponentRegistry: + """Auto-discovery and management of installable components""" + + def __init__(self, components_dir: Path): + """ + Initialize component registry + + Args: + components_dir: Directory containing component modules + """ + self.components_dir = components_dir + self.component_classes: Dict[str, Type[Component]] = {} + self.component_instances: Dict[str, Component] = {} + self.dependency_graph: Dict[str, Set[str]] = {} + self._discovered = False + + def discover_components(self, force_reload: bool = False) -> None: + """ + Auto-discover all component classes in components directory + + Args: + force_reload: Force rediscovery even if already done + """ + if self._discovered and not force_reload: + return + + self.component_classes.clear() + self.component_instances.clear() + self.dependency_graph.clear() + + if not self.components_dir.exists(): + return + + # Add components directory to Python path temporarily + import sys + original_path = sys.path.copy() + + try: + # Add parent directory to path so we can import setup.components + setup_dir = self.components_dir.parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + + # Discover all Python files in components directory + for py_file in self.components_dir.glob("*.py"): + if py_file.name.startswith("__"): + continue + + module_name = py_file.stem + self._load_component_module(module_name) + + finally: + # Restore original Python path + sys.path = original_path + + # Build dependency graph + self._build_dependency_graph() + self._discovered = True + + def _load_component_module(self, module_name: str) -> None: + """ + Load component classes from a module + + Args: + module_name: Name of module to load + """ + try: + # Import the module + full_module_name = f"setup.components.{module_name}" + module = importlib.import_module(full_module_name) + + # Find all Component subclasses in the module + for name, obj in inspect.getmembers(module): + if (inspect.isclass(obj) and + issubclass(obj, Component) and + obj is not Component): + + # Create instance to get metadata + try: + instance = obj() + metadata = instance.get_metadata() + component_name = metadata["name"] + + self.component_classes[component_name] = obj + self.component_instances[component_name] = instance + + except Exception as e: + print(f"Warning: Could not instantiate component {name}: {e}") + + except Exception as e: + print(f"Warning: Could not load component module {module_name}: {e}") + + def _build_dependency_graph(self) -> None: + """Build dependency graph for all discovered components""" + for name, instance in self.component_instances.items(): + try: + dependencies = instance.get_dependencies() + self.dependency_graph[name] = set(dependencies) + except Exception as e: + print(f"Warning: Could not get dependencies for {name}: {e}") + self.dependency_graph[name] = set() + + def get_component_class(self, component_name: str) -> Optional[Type[Component]]: + """ + Get component class by name + + Args: + component_name: Name of component + + Returns: + Component class or None if not found + """ + self.discover_components() + return self.component_classes.get(component_name) + + def get_component_instance(self, component_name: str, install_dir: Optional[Path] = None) -> Optional[Component]: + """ + Get component instance by name + + Args: + component_name: Name of component + install_dir: Installation directory (creates new instance with this dir) + + Returns: + Component instance or None if not found + """ + self.discover_components() + + if install_dir is not None: + # Create new instance with specified install directory + component_class = self.component_classes.get(component_name) + if component_class: + try: + return component_class(install_dir) + except Exception as e: + print(f"Error creating component instance {component_name}: {e}") + return None + + return self.component_instances.get(component_name) + + def list_components(self) -> List[str]: + """ + Get list of all discovered component names + + Returns: + List of component names + """ + self.discover_components() + return list(self.component_classes.keys()) + + def get_component_metadata(self, component_name: str) -> Optional[Dict[str, str]]: + """ + Get metadata for a component + + Args: + component_name: Name of component + + Returns: + Component metadata dict or None if not found + """ + self.discover_components() + instance = self.component_instances.get(component_name) + if instance: + try: + return instance.get_metadata() + except Exception: + return None + return None + + def resolve_dependencies(self, component_names: List[str]) -> List[str]: + """ + Resolve component dependencies in correct installation order + + Args: + component_names: List of component names to install + + Returns: + Ordered list of component names including dependencies + + Raises: + ValueError: If circular dependencies detected or unknown component + """ + self.discover_components() + + resolved = [] + resolving = set() + + def resolve(name: str): + if name in resolved: + return + + if name in resolving: + raise ValueError(f"Circular dependency detected involving {name}") + + if name not in self.dependency_graph: + raise ValueError(f"Unknown component: {name}") + + resolving.add(name) + + # Resolve dependencies first + for dep in self.dependency_graph[name]: + resolve(dep) + + resolving.remove(name) + resolved.append(name) + + # Resolve each requested component + for name in component_names: + resolve(name) + + return resolved + + def get_dependencies(self, component_name: str) -> Set[str]: + """ + Get direct dependencies for a component + + Args: + component_name: Name of component + + Returns: + Set of dependency component names + """ + self.discover_components() + return self.dependency_graph.get(component_name, set()) + + def get_dependents(self, component_name: str) -> Set[str]: + """ + Get components that depend on the given component + + Args: + component_name: Name of component + + Returns: + Set of component names that depend on this component + """ + self.discover_components() + dependents = set() + + for name, deps in self.dependency_graph.items(): + if component_name in deps: + dependents.add(name) + + return dependents + + def validate_dependency_graph(self) -> List[str]: + """ + Validate dependency graph for cycles and missing dependencies + + Returns: + List of validation errors (empty if valid) + """ + self.discover_components() + errors = [] + + # Check for missing dependencies + all_components = set(self.dependency_graph.keys()) + for name, deps in self.dependency_graph.items(): + missing_deps = deps - all_components + if missing_deps: + errors.append(f"Component {name} has missing dependencies: {missing_deps}") + + # Check for circular dependencies + for name in all_components: + try: + self.resolve_dependencies([name]) + except ValueError as e: + errors.append(str(e)) + + return errors + + def get_components_by_category(self, category: str) -> List[str]: + """ + Get components filtered by category + + Args: + category: Component category to filter by + + Returns: + List of component names in the category + """ + self.discover_components() + components = [] + + for name, instance in self.component_instances.items(): + try: + metadata = instance.get_metadata() + if metadata.get("category") == category: + components.append(name) + except Exception: + continue + + return components + + def get_installation_order(self, component_names: List[str]) -> List[List[str]]: + """ + Get installation order grouped by dependency levels + + Args: + component_names: List of component names to install + + Returns: + List of lists, where each inner list contains components + that can be installed in parallel at that dependency level + """ + self.discover_components() + + # Get all components including dependencies + all_components = set(self.resolve_dependencies(component_names)) + + # Group by dependency level + levels = [] + remaining = all_components.copy() + + while remaining: + # Find components with no unresolved dependencies + current_level = [] + for name in list(remaining): + deps = self.dependency_graph.get(name, set()) + unresolved_deps = deps & remaining + + if not unresolved_deps: + current_level.append(name) + + if not current_level: + # This shouldn't happen if dependency graph is valid + raise ValueError("Circular dependency detected in installation order calculation") + + levels.append(current_level) + remaining -= set(current_level) + + return levels + + def create_component_instances(self, component_names: List[str], install_dir: Optional[Path] = None) -> Dict[str, Component]: + """ + Create instances for multiple components + + Args: + component_names: List of component names + install_dir: Installation directory for instances + + Returns: + Dict mapping component names to instances + """ + self.discover_components() + instances = {} + + for name in component_names: + instance = self.get_component_instance(name, install_dir) + if instance: + instances[name] = instance + else: + print(f"Warning: Could not create instance for component {name}") + + return instances + + def get_registry_info(self) -> Dict[str, any]: + """ + Get comprehensive registry information + + Returns: + Dict with registry statistics and component info + """ + self.discover_components() + + # Group components by category + categories = {} + for name, instance in self.component_instances.items(): + try: + metadata = instance.get_metadata() + category = metadata.get("category", "unknown") + if category not in categories: + categories[category] = [] + categories[category].append(name) + except Exception: + if "unknown" not in categories: + categories["unknown"] = [] + categories["unknown"].append(name) + + return { + "total_components": len(self.component_classes), + "categories": categories, + "dependency_graph": {name: list(deps) for name, deps in self.dependency_graph.items()}, + "validation_errors": self.validate_dependency_graph() + } \ No newline at end of file diff --git a/setup/core/validator.py b/setup/core/validator.py new file mode 100644 index 0000000..1929fcb --- /dev/null +++ b/setup/core/validator.py @@ -0,0 +1,685 @@ +""" +System validation for SuperClaude installation requirements +""" + +import subprocess +import sys +import shutil +from typing import Tuple, List, Dict, Any, Optional +from pathlib import Path +import re + +# Handle packaging import - if not available, use a simple version comparison +try: + from packaging import version + PACKAGING_AVAILABLE = True +except ImportError: + PACKAGING_AVAILABLE = False + + class SimpleVersion: + def __init__(self, version_str: str): + self.version_str = version_str + # Simple version parsing: split by dots and convert to integers + try: + self.parts = [int(x) for x in version_str.split('.')] + except ValueError: + self.parts = [0, 0, 0] + + def __lt__(self, other): + if isinstance(other, str): + other = SimpleVersion(other) + # Pad with zeros to same length + max_len = max(len(self.parts), len(other.parts)) + self_parts = self.parts + [0] * (max_len - len(self.parts)) + other_parts = other.parts + [0] * (max_len - len(other.parts)) + return self_parts < other_parts + + def __gt__(self, other): + if isinstance(other, str): + other = SimpleVersion(other) + return not (self < other) and not (self == other) + + def __eq__(self, other): + if isinstance(other, str): + other = SimpleVersion(other) + return self.parts == other.parts + + class version: + @staticmethod + def parse(version_str: str): + return SimpleVersion(version_str) + + +class Validator: + """System requirements validator""" + + def __init__(self): + """Initialize validator""" + self.validation_cache: Dict[str, Any] = {} + + def check_python(self, min_version: str = "3.8", max_version: Optional[str] = None) -> Tuple[bool, str]: + """ + Check Python version requirements + + Args: + min_version: Minimum required Python version + max_version: Maximum supported Python version (optional) + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"python_{min_version}_{max_version}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Get current Python version + current_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + + # Check minimum version + if version.parse(current_version) < version.parse(min_version): + help_msg = self.get_installation_help("python") + result = (False, f"Python {min_version}+ required, found {current_version}{help_msg}") + self.validation_cache[cache_key] = result + return result + + # Check maximum version if specified + if max_version and version.parse(current_version) > version.parse(max_version): + result = (False, f"Python version {current_version} exceeds maximum supported {max_version}") + self.validation_cache[cache_key] = result + return result + + result = (True, f"Python {current_version} meets requirements") + self.validation_cache[cache_key] = result + return result + + except Exception as e: + result = (False, f"Could not check Python version: {e}") + self.validation_cache[cache_key] = result + return result + + def check_node(self, min_version: str = "16.0", max_version: Optional[str] = None) -> Tuple[bool, str]: + """ + Check Node.js version requirements + + Args: + min_version: Minimum required Node.js version + max_version: Maximum supported Node.js version (optional) + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"node_{min_version}_{max_version}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Check if node is installed - use shell=True on Windows for better PATH resolution + result = subprocess.run( + ['node', '--version'], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + help_msg = self.get_installation_help("node") + result_tuple = (False, f"Node.js not found in PATH{help_msg}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + # Parse version (format: v18.17.0) + version_output = result.stdout.strip() + if version_output.startswith('v'): + current_version = version_output[1:] + else: + current_version = version_output + + # Check minimum version + if version.parse(current_version) < version.parse(min_version): + help_msg = self.get_installation_help("node") + result_tuple = (False, f"Node.js {min_version}+ required, found {current_version}{help_msg}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + # Check maximum version if specified + if max_version and version.parse(current_version) > version.parse(max_version): + result_tuple = (False, f"Node.js version {current_version} exceeds maximum supported {max_version}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + result_tuple = (True, f"Node.js {current_version} meets requirements") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + except subprocess.TimeoutExpired: + result_tuple = (False, "Node.js version check timed out") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except FileNotFoundError: + help_msg = self.get_installation_help("node") + result_tuple = (False, f"Node.js not found in PATH{help_msg}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except Exception as e: + result_tuple = (False, f"Could not check Node.js version: {e}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + def check_claude_cli(self, min_version: Optional[str] = None) -> Tuple[bool, str]: + """ + Check Claude CLI installation and version + + Args: + min_version: Minimum required Claude CLI version (optional) + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"claude_cli_{min_version}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Check if claude is installed - use shell=True on Windows for better PATH resolution + result = subprocess.run( + ['claude', '--version'], + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + help_msg = self.get_installation_help("claude_cli") + result_tuple = (False, f"Claude CLI not found in PATH{help_msg}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + # Parse version from output + version_output = result.stdout.strip() + version_match = re.search(r'(\d+\.\d+\.\d+)', version_output) + + if not version_match: + result_tuple = (True, "Claude CLI found (version format unknown)") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + current_version = version_match.group(1) + + # Check minimum version if specified + if min_version and version.parse(current_version) < version.parse(min_version): + result_tuple = (False, f"Claude CLI {min_version}+ required, found {current_version}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + result_tuple = (True, f"Claude CLI {current_version} found") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + except subprocess.TimeoutExpired: + result_tuple = (False, "Claude CLI version check timed out") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except FileNotFoundError: + help_msg = self.get_installation_help("claude_cli") + result_tuple = (False, f"Claude CLI not found in PATH{help_msg}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except Exception as e: + result_tuple = (False, f"Could not check Claude CLI: {e}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + def check_external_tool(self, tool_name: str, command: str, min_version: Optional[str] = None) -> Tuple[bool, str]: + """ + Check external tool availability and version + + Args: + tool_name: Display name of tool + command: Command to check version + min_version: Minimum required version (optional) + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"tool_{tool_name}_{command}_{min_version}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Split command into parts + cmd_parts = command.split() + + result = subprocess.run( + cmd_parts, + capture_output=True, + text=True, + timeout=10, + shell=(sys.platform == "win32") + ) + + if result.returncode != 0: + result_tuple = (False, f"{tool_name} not found or command failed") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + # Extract version if min_version specified + if min_version: + version_output = result.stdout + result.stderr + version_match = re.search(r'(\d+\.\d+(?:\.\d+)?)', version_output) + + if version_match: + current_version = version_match.group(1) + + if version.parse(current_version) < version.parse(min_version): + result_tuple = (False, f"{tool_name} {min_version}+ required, found {current_version}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + result_tuple = (True, f"{tool_name} {current_version} found") + self.validation_cache[cache_key] = result_tuple + return result_tuple + else: + result_tuple = (True, f"{tool_name} found (version unknown)") + self.validation_cache[cache_key] = result_tuple + return result_tuple + else: + result_tuple = (True, f"{tool_name} found") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + except subprocess.TimeoutExpired: + result_tuple = (False, f"{tool_name} check timed out") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except FileNotFoundError: + result_tuple = (False, f"{tool_name} not found in PATH") + self.validation_cache[cache_key] = result_tuple + return result_tuple + except Exception as e: + result_tuple = (False, f"Could not check {tool_name}: {e}") + self.validation_cache[cache_key] = result_tuple + return result_tuple + + def check_disk_space(self, path: Path, required_mb: int = 500) -> Tuple[bool, str]: + """ + Check available disk space + + Args: + path: Path to check (file or directory) + required_mb: Required free space in MB + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"disk_{path}_{required_mb}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Get parent directory if path is a file + check_path = path.parent if path.is_file() else path + + # Get disk usage + stat_result = shutil.disk_usage(check_path) + free_mb = stat_result.free / (1024 * 1024) + + if free_mb < required_mb: + result = (False, f"Insufficient disk space: {free_mb:.1f}MB free, {required_mb}MB required") + else: + result = (True, f"Sufficient disk space: {free_mb:.1f}MB free") + + self.validation_cache[cache_key] = result + return result + + except Exception as e: + result = (False, f"Could not check disk space: {e}") + self.validation_cache[cache_key] = result + return result + + def check_write_permissions(self, path: Path) -> Tuple[bool, str]: + """ + Check write permissions for path + + Args: + path: Path to check + + Returns: + Tuple of (success: bool, message: str) + """ + cache_key = f"write_{path}" + if cache_key in self.validation_cache: + return self.validation_cache[cache_key] + + try: + # Create parent directories if needed + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + + # Test write access + test_file = path / ".write_test" + test_file.touch() + test_file.unlink() + + result = (True, f"Write access confirmed for {path}") + self.validation_cache[cache_key] = result + return result + + except Exception as e: + result = (False, f"No write access to {path}: {e}") + self.validation_cache[cache_key] = result + return result + + def validate_requirements(self, requirements: Dict[str, Any]) -> Tuple[bool, List[str]]: + """ + Validate all system requirements + + Args: + requirements: Requirements configuration dict + + Returns: + Tuple of (all_passed: bool, error_messages: List[str]) + """ + errors = [] + + # Check Python requirements + if "python" in requirements: + python_req = requirements["python"] + success, message = self.check_python( + python_req["min_version"], + python_req.get("max_version") + ) + if not success: + errors.append(f"Python: {message}") + + # Check Node.js requirements + if "node" in requirements: + node_req = requirements["node"] + success, message = self.check_node( + node_req["min_version"], + node_req.get("max_version") + ) + if not success: + errors.append(f"Node.js: {message}") + + # Check disk space + if "disk_space_mb" in requirements: + success, message = self.check_disk_space( + Path.home(), + requirements["disk_space_mb"] + ) + if not success: + errors.append(f"Disk space: {message}") + + # Check external tools + if "external_tools" in requirements: + for tool_name, tool_req in requirements["external_tools"].items(): + # Skip optional tools that fail + is_optional = tool_req.get("optional", False) + + success, message = self.check_external_tool( + tool_name, + tool_req["command"], + tool_req.get("min_version") + ) + + if not success and not is_optional: + errors.append(f"{tool_name}: {message}") + + return len(errors) == 0, errors + + def validate_component_requirements(self, component_names: List[str], all_requirements: Dict[str, Any]) -> Tuple[bool, List[str]]: + """ + Validate requirements for specific components + + Args: + component_names: List of component names to validate + all_requirements: Full requirements configuration + + Returns: + Tuple of (all_passed: bool, error_messages: List[str]) + """ + errors = [] + + # Start with base requirements + base_requirements = { + "python": all_requirements.get("python", {}), + "disk_space_mb": all_requirements.get("disk_space_mb", 500) + } + + # Add conditional requirements based on components + external_tools = {} + + # Check if any component needs Node.js + node_components = [] + for component in component_names: + # This would be enhanced with actual component metadata + if component in ["mcp"]: # MCP component needs Node.js + node_components.append(component) + + if node_components and "node" in all_requirements: + base_requirements["node"] = all_requirements["node"] + + # Add external tools needed by components + if "external_tools" in all_requirements: + for tool_name, tool_req in all_requirements["external_tools"].items(): + required_for = tool_req.get("required_for", []) + + # Check if any of our components need this tool + if any(comp in required_for for comp in component_names): + external_tools[tool_name] = tool_req + + if external_tools: + base_requirements["external_tools"] = external_tools + + # Validate consolidated requirements + return self.validate_requirements(base_requirements) + + def get_system_info(self) -> Dict[str, Any]: + """ + Get comprehensive system information + + Returns: + Dict with system information + """ + info = { + "platform": sys.platform, + "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + "python_executable": sys.executable + } + + # Add Node.js info if available + node_success, node_msg = self.check_node() + info["node_available"] = node_success + if node_success: + info["node_message"] = node_msg + + # Add Claude CLI info if available + claude_success, claude_msg = self.check_claude_cli() + info["claude_cli_available"] = claude_success + if claude_success: + info["claude_cli_message"] = claude_msg + + # Add disk space info + try: + home_path = Path.home() + stat_result = shutil.disk_usage(home_path) + info["disk_space"] = { + "total_gb": stat_result.total / (1024**3), + "free_gb": stat_result.free / (1024**3), + "used_gb": (stat_result.total - stat_result.free) / (1024**3) + } + except Exception: + info["disk_space"] = {"error": "Could not determine disk space"} + + return info + + def get_platform(self) -> str: + """ + Get current platform for installation commands + + Returns: + Platform string (linux, darwin, win32) + """ + return sys.platform + + def load_installation_commands(self) -> Dict[str, Any]: + """ + Load installation commands from requirements configuration + + Returns: + Installation commands dict + """ + try: + from ..managers.config_manager import ConfigManager + from .. import PROJECT_ROOT + + config_manager = ConfigManager(PROJECT_ROOT / "config") + requirements = config_manager.load_requirements() + return requirements.get("installation_commands", {}) + except Exception: + return {} + + def get_installation_help(self, tool_name: str, platform: Optional[str] = None) -> str: + """ + Get installation help for a specific tool + + Args: + tool_name: Name of tool to get help for + platform: Target platform (auto-detected if None) + + Returns: + Installation help string + """ + if platform is None: + platform = self.get_platform() + + commands = self.load_installation_commands() + tool_commands = commands.get(tool_name, {}) + + if not tool_commands: + return f"No installation instructions available for {tool_name}" + + # Get platform-specific command or fallback to 'all' + install_cmd = tool_commands.get(platform, tool_commands.get("all", "")) + description = tool_commands.get("description", "") + + if install_cmd: + help_text = f"\n💡 Installation Help for {tool_name}:\n" + if description: + help_text += f" {description}\n" + help_text += f" Command: {install_cmd}\n" + return help_text + + return f"No installation instructions available for {tool_name} on {platform}" + + def diagnose_system(self) -> Dict[str, Any]: + """ + Perform comprehensive system diagnostics + + Returns: + Diagnostic information dict + """ + diagnostics = { + "platform": self.get_platform(), + "checks": {}, + "issues": [], + "recommendations": [] + } + + # Check Python + python_success, python_msg = self.check_python() + diagnostics["checks"]["python"] = { + "status": "pass" if python_success else "fail", + "message": python_msg + } + if not python_success: + diagnostics["issues"].append("Python version issue") + diagnostics["recommendations"].append(self.get_installation_help("python")) + + # Check Node.js + node_success, node_msg = self.check_node() + diagnostics["checks"]["node"] = { + "status": "pass" if node_success else "fail", + "message": node_msg + } + if not node_success: + diagnostics["issues"].append("Node.js not found or version issue") + diagnostics["recommendations"].append(self.get_installation_help("node")) + + # Check Claude CLI + claude_success, claude_msg = self.check_claude_cli() + diagnostics["checks"]["claude_cli"] = { + "status": "pass" if claude_success else "fail", + "message": claude_msg + } + if not claude_success: + diagnostics["issues"].append("Claude CLI not found") + diagnostics["recommendations"].append(self.get_installation_help("claude_cli")) + + # Check disk space + disk_success, disk_msg = self.check_disk_space(Path.home()) + diagnostics["checks"]["disk_space"] = { + "status": "pass" if disk_success else "fail", + "message": disk_msg + } + if not disk_success: + diagnostics["issues"].append("Insufficient disk space") + + # Check common PATH issues + self._diagnose_path_issues(diagnostics) + + return diagnostics + + def _diagnose_path_issues(self, diagnostics: Dict[str, Any]) -> None: + """Add PATH-related diagnostics""" + path_issues = [] + + # Check if tools are in PATH, with alternatives for some tools + tool_checks = [ + # For Python, check if either python3 OR python is available + (["python3", "python"], "Python (python3 or python)"), + (["node"], "Node.js"), + (["npm"], "npm"), + (["claude"], "Claude CLI") + ] + + for tool_alternatives, display_name in tool_checks: + tool_found = False + for tool in tool_alternatives: + try: + result = subprocess.run( + ["which" if sys.platform != "win32" else "where", tool], + capture_output=True, + text=True, + timeout=5, + shell=(sys.platform == "win32") + ) + if result.returncode == 0: + tool_found = True + break + except Exception: + continue + + if not tool_found: + # Only report as missing if none of the alternatives were found + if len(tool_alternatives) > 1: + path_issues.append(f"{display_name} not found in PATH") + else: + path_issues.append(f"{tool_alternatives[0]} not found in PATH") + + if path_issues: + diagnostics["issues"].extend(path_issues) + diagnostics["recommendations"].append( + "\n💡 PATH Issue Help:\n" + " Some tools may not be in your PATH. Try:\n" + " - Restart your terminal after installation\n" + " - Check your shell configuration (.bashrc, .zshrc)\n" + " - Use full paths to tools if needed\n" + ) + + def clear_cache(self) -> None: + """Clear validation cache""" + self.validation_cache.clear() diff --git a/setup/managers/__init__.py b/setup/managers/__init__.py new file mode 100644 index 0000000..f179dd6 --- /dev/null +++ b/setup/managers/__init__.py @@ -0,0 +1,9 @@ +from .config_manager import ConfigManager +from .settings_manager import SettingsManager +from .file_manager import FileManager + +__all__ = [ + 'ConfigManager', + 'SettingsManager', + 'FileManager' +] diff --git a/setup/managers/config_manager.py b/setup/managers/config_manager.py new file mode 100644 index 0000000..ccfac0a --- /dev/null +++ b/setup/managers/config_manager.py @@ -0,0 +1,399 @@ +""" +Configuration management for SuperClaude installation system +""" + +import json +from typing import Dict, Any, List, Optional +from pathlib import Path + +# Handle jsonschema import - if not available, use basic validation +try: + import jsonschema + from jsonschema import validate, ValidationError + JSONSCHEMA_AVAILABLE = True +except ImportError: + JSONSCHEMA_AVAILABLE = False + + class ValidationError(Exception): + """Simple validation error for when jsonschema is not available""" + def __init__(self, message): + self.message = message + super().__init__(message) + + def validate(instance, schema): + """Dummy validation function""" + # Basic type checking only + if "type" in schema: + expected_type = schema["type"] + if expected_type == "object" and not isinstance(instance, dict): + raise ValidationError(f"Expected object, got {type(instance).__name__}") + elif expected_type == "array" and not isinstance(instance, list): + raise ValidationError(f"Expected array, got {type(instance).__name__}") + elif expected_type == "string" and not isinstance(instance, str): + raise ValidationError(f"Expected string, got {type(instance).__name__}") + elif expected_type == "integer" and not isinstance(instance, int): + raise ValidationError(f"Expected integer, got {type(instance).__name__}") + # Skip detailed validation if jsonschema not available + + +class ConfigManager: + """Manages configuration files and validation""" + + def __init__(self, config_dir: Path): + """ + Initialize config manager + + Args: + config_dir: Directory containing configuration files + """ + self.config_dir = config_dir + self.features_file = config_dir / "features.json" + self.requirements_file = config_dir / "requirements.json" + self._features_cache = None + self._requirements_cache = None + + # Schema for features.json + self.features_schema = { + "type": "object", + "properties": { + "components": { + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "version": {"type": "string"}, + "description": {"type": "string"}, + "category": {"type": "string"}, + "dependencies": { + "type": "array", + "items": {"type": "string"} + }, + "enabled": {"type": "boolean"}, + "required_tools": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["name", "version", "description", "category"], + "additionalProperties": False + } + } + } + }, + "required": ["components"], + "additionalProperties": False + } + + # Schema for requirements.json + self.requirements_schema = { + "type": "object", + "properties": { + "python": { + "type": "object", + "properties": { + "min_version": {"type": "string"}, + "max_version": {"type": "string"} + }, + "required": ["min_version"] + }, + "node": { + "type": "object", + "properties": { + "min_version": {"type": "string"}, + "max_version": {"type": "string"}, + "required_for": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["min_version"] + }, + "disk_space_mb": {"type": "integer"}, + "external_tools": { + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_-]*$": { + "type": "object", + "properties": { + "command": {"type": "string"}, + "min_version": {"type": "string"}, + "required_for": { + "type": "array", + "items": {"type": "string"} + }, + "optional": {"type": "boolean"} + }, + "required": ["command"], + "additionalProperties": False + } + } + }, + "installation_commands": { + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_-]*$": { + "type": "object", + "properties": { + "linux": {"type": "string"}, + "darwin": {"type": "string"}, + "win32": {"type": "string"}, + "all": {"type": "string"}, + "description": {"type": "string"} + }, + "additionalProperties": False + } + } + } + }, + "required": ["python", "disk_space_mb"], + "additionalProperties": False + } + + def load_features(self) -> Dict[str, Any]: + """ + Load and validate features configuration + + Returns: + Features configuration dict + + Raises: + FileNotFoundError: If features.json not found + ValidationError: If features.json is invalid + """ + if self._features_cache is not None: + return self._features_cache + + if not self.features_file.exists(): + raise FileNotFoundError(f"Features config not found: {self.features_file}") + + try: + with open(self.features_file, 'r') as f: + features = json.load(f) + + # Validate schema + validate(instance=features, schema=self.features_schema) + + self._features_cache = features + return features + + except json.JSONDecodeError as e: + raise ValidationError(f"Invalid JSON in {self.features_file}: {e}") + except ValidationError as e: + raise ValidationError(f"Invalid features schema: {e.message}") + + def load_requirements(self) -> Dict[str, Any]: + """ + Load and validate requirements configuration + + Returns: + Requirements configuration dict + + Raises: + FileNotFoundError: If requirements.json not found + ValidationError: If requirements.json is invalid + """ + if self._requirements_cache is not None: + return self._requirements_cache + + if not self.requirements_file.exists(): + raise FileNotFoundError(f"Requirements config not found: {self.requirements_file}") + + try: + with open(self.requirements_file, 'r') as f: + requirements = json.load(f) + + # Validate schema + validate(instance=requirements, schema=self.requirements_schema) + + self._requirements_cache = requirements + return requirements + + except json.JSONDecodeError as e: + raise ValidationError(f"Invalid JSON in {self.requirements_file}: {e}") + except ValidationError as e: + raise ValidationError(f"Invalid requirements schema: {e.message}") + + def get_component_info(self, component_name: str) -> Optional[Dict[str, Any]]: + """ + Get information about a specific component + + Args: + component_name: Name of component + + Returns: + Component info dict or None if not found + """ + features = self.load_features() + return features.get("components", {}).get(component_name) + + def get_enabled_components(self) -> List[str]: + """ + Get list of enabled component names + + Returns: + List of enabled component names + """ + features = self.load_features() + enabled = [] + + for name, info in features.get("components", {}).items(): + if info.get("enabled", True): # Default to enabled + enabled.append(name) + + return enabled + + def get_components_by_category(self, category: str) -> List[str]: + """ + Get component names by category + + Args: + category: Component category + + Returns: + List of component names in category + """ + features = self.load_features() + components = [] + + for name, info in features.get("components", {}).items(): + if info.get("category") == category: + components.append(name) + + return components + + def get_component_dependencies(self, component_name: str) -> List[str]: + """ + Get dependencies for a component + + Args: + component_name: Name of component + + Returns: + List of dependency component names + """ + component_info = self.get_component_info(component_name) + if component_info: + return component_info.get("dependencies", []) + return [] + + def load_profile(self, profile_path: Path) -> Dict[str, Any]: + """ + Load installation profile + + Args: + profile_path: Path to profile JSON file + + Returns: + Profile configuration dict + + Raises: + FileNotFoundError: If profile not found + ValidationError: If profile is invalid + """ + if not profile_path.exists(): + raise FileNotFoundError(f"Profile not found: {profile_path}") + + try: + with open(profile_path, 'r') as f: + profile = json.load(f) + + # Basic validation + if "components" not in profile: + raise ValidationError("Profile must contain 'components' field") + + if not isinstance(profile["components"], list): + raise ValidationError("Profile 'components' must be a list") + + # Validate that all components exist + features = self.load_features() + available_components = set(features.get("components", {}).keys()) + + for component in profile["components"]: + if component not in available_components: + raise ValidationError(f"Unknown component in profile: {component}") + + return profile + + except json.JSONDecodeError as e: + raise ValidationError(f"Invalid JSON in {profile_path}: {e}") + + def get_system_requirements(self) -> Dict[str, Any]: + """ + Get system requirements + + Returns: + System requirements dict + """ + return self.load_requirements() + + def get_requirements_for_components(self, component_names: List[str]) -> Dict[str, Any]: + """ + Get consolidated requirements for specific components + + Args: + component_names: List of component names + + Returns: + Consolidated requirements dict + """ + requirements = self.load_requirements() + features = self.load_features() + + # Start with base requirements + result = { + "python": requirements["python"], + "disk_space_mb": requirements["disk_space_mb"], + "external_tools": {} + } + + # Add Node.js requirements if needed + node_required = False + for component_name in component_names: + component_info = features.get("components", {}).get(component_name, {}) + required_tools = component_info.get("required_tools", []) + + if "node" in required_tools: + node_required = True + break + + if node_required and "node" in requirements: + result["node"] = requirements["node"] + + # Add external tool requirements + for component_name in component_names: + component_info = features.get("components", {}).get(component_name, {}) + required_tools = component_info.get("required_tools", []) + + for tool in required_tools: + if tool in requirements.get("external_tools", {}): + result["external_tools"][tool] = requirements["external_tools"][tool] + + return result + + def validate_config_files(self) -> List[str]: + """ + Validate all configuration files + + Returns: + List of validation errors (empty if all valid) + """ + errors = [] + + try: + self.load_features() + except Exception as e: + errors.append(f"Features config error: {e}") + + try: + self.load_requirements() + except Exception as e: + errors.append(f"Requirements config error: {e}") + + return errors + + def clear_cache(self) -> None: + """Clear cached configuration data""" + self._features_cache = None + self._requirements_cache = None \ No newline at end of file diff --git a/setup/managers/file_manager.py b/setup/managers/file_manager.py new file mode 100644 index 0000000..f637610 --- /dev/null +++ b/setup/managers/file_manager.py @@ -0,0 +1,428 @@ +""" +Cross-platform file management for SuperClaude installation system +""" + +import shutil +import stat +from typing import List, Optional, Callable, Dict, Any +from pathlib import Path +import fnmatch +import hashlib + + +class FileManager: + """Cross-platform file operations manager""" + + def __init__(self, dry_run: bool = False): + """ + Initialize file manager + + Args: + dry_run: If True, only simulate file operations + """ + self.dry_run = dry_run + self.copied_files: List[Path] = [] + self.created_dirs: List[Path] = [] + + def copy_file(self, source: Path, target: Path, preserve_permissions: bool = True) -> bool: + """ + Copy single file with permission preservation + + Args: + source: Source file path + target: Target file path + preserve_permissions: Whether to preserve file permissions + + Returns: + True if successful, False otherwise + """ + if not source.exists(): + raise FileNotFoundError(f"Source file not found: {source}") + + if not source.is_file(): + raise ValueError(f"Source is not a file: {source}") + + if self.dry_run: + print(f"[DRY RUN] Would copy {source} -> {target}") + return True + + try: + # Ensure target directory exists + target.parent.mkdir(parents=True, exist_ok=True) + + # Copy file + if preserve_permissions: + shutil.copy2(source, target) + else: + shutil.copy(source, target) + + self.copied_files.append(target) + return True + + except Exception as e: + print(f"Error copying {source} to {target}: {e}") + return False + + def copy_directory(self, source: Path, target: Path, ignore_patterns: Optional[List[str]] = None) -> bool: + """ + Recursively copy directory with gitignore-style patterns + + Args: + source: Source directory path + target: Target directory path + ignore_patterns: List of patterns to ignore (gitignore style) + + Returns: + True if successful, False otherwise + """ + if not source.exists(): + raise FileNotFoundError(f"Source directory not found: {source}") + + if not source.is_dir(): + raise ValueError(f"Source is not a directory: {source}") + + ignore_patterns = ignore_patterns or [] + default_ignores = ['.git', '.gitignore', '__pycache__', '*.pyc', '.DS_Store'] + all_ignores = ignore_patterns + default_ignores + + if self.dry_run: + print(f"[DRY RUN] Would copy directory {source} -> {target}") + return True + + try: + # Create ignore function + def ignore_func(directory: str, contents: List[str]) -> List[str]: + ignored = [] + for item in contents: + item_path = Path(directory) / item + rel_path = item_path.relative_to(source) + + # Check against ignore patterns + for pattern in all_ignores: + if fnmatch.fnmatch(item, pattern) or fnmatch.fnmatch(str(rel_path), pattern): + ignored.append(item) + break + + return ignored + + # Copy tree + shutil.copytree(source, target, ignore=ignore_func, dirs_exist_ok=True) + + # Track created directories and files + for item in target.rglob('*'): + if item.is_dir(): + self.created_dirs.append(item) + else: + self.copied_files.append(item) + + return True + + except Exception as e: + print(f"Error copying directory {source} to {target}: {e}") + return False + + def ensure_directory(self, directory: Path, mode: int = 0o755) -> bool: + """ + Create directory and parents if they don't exist + + Args: + directory: Directory path to create + mode: Directory permissions (Unix only) + + Returns: + True if successful, False otherwise + """ + if self.dry_run: + print(f"[DRY RUN] Would create directory {directory}") + return True + + try: + directory.mkdir(parents=True, exist_ok=True, mode=mode) + + if directory not in self.created_dirs: + self.created_dirs.append(directory) + + return True + + except Exception as e: + print(f"Error creating directory {directory}: {e}") + return False + + def remove_file(self, file_path: Path) -> bool: + """ + Remove single file + + Args: + file_path: Path to file to remove + + Returns: + True if successful, False otherwise + """ + if not file_path.exists(): + return True # Already gone + + if self.dry_run: + print(f"[DRY RUN] Would remove file {file_path}") + return True + + try: + if file_path.is_file(): + file_path.unlink() + else: + print(f"Warning: {file_path} is not a file, skipping") + return False + + # Remove from tracking + if file_path in self.copied_files: + self.copied_files.remove(file_path) + + return True + + except Exception as e: + print(f"Error removing file {file_path}: {e}") + return False + + def remove_directory(self, directory: Path, recursive: bool = False) -> bool: + """ + Remove directory + + Args: + directory: Directory path to remove + recursive: Whether to remove recursively + + Returns: + True if successful, False otherwise + """ + if not directory.exists(): + return True # Already gone + + if self.dry_run: + action = "recursively remove" if recursive else "remove" + print(f"[DRY RUN] Would {action} directory {directory}") + return True + + try: + if recursive: + shutil.rmtree(directory) + else: + directory.rmdir() # Only works if empty + + # Remove from tracking + if directory in self.created_dirs: + self.created_dirs.remove(directory) + + return True + + except Exception as e: + print(f"Error removing directory {directory}: {e}") + return False + + def resolve_home_path(self, path: str) -> Path: + """ + Convert path with ~ to actual home path on any OS + + Args: + path: Path string potentially containing ~ + + Returns: + Resolved Path object + """ + return Path(path).expanduser().resolve() + + def make_executable(self, file_path: Path) -> bool: + """ + Make file executable (Unix/Linux/macOS) + + Args: + file_path: Path to file to make executable + + Returns: + True if successful, False otherwise + """ + if not file_path.exists(): + return False + + if self.dry_run: + print(f"[DRY RUN] Would make {file_path} executable") + return True + + try: + # Get current permissions + current_mode = file_path.stat().st_mode + + # Add execute permissions for owner, group, and others + new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + + file_path.chmod(new_mode) + return True + + except Exception as e: + print(f"Error making {file_path} executable: {e}") + return False + + def get_file_hash(self, file_path: Path, algorithm: str = 'sha256') -> Optional[str]: + """ + Calculate file hash + + Args: + file_path: Path to file + algorithm: Hash algorithm (md5, sha1, sha256, etc.) + + Returns: + Hex hash string or None if error + """ + if not file_path.exists() or not file_path.is_file(): + return None + + try: + hasher = hashlib.new(algorithm) + + with open(file_path, 'rb') as f: + # Read in chunks for large files + for chunk in iter(lambda: f.read(8192), b""): + hasher.update(chunk) + + return hasher.hexdigest() + + except Exception: + return None + + def verify_file_integrity(self, file_path: Path, expected_hash: str, algorithm: str = 'sha256') -> bool: + """ + Verify file integrity using hash + + Args: + file_path: Path to file to verify + expected_hash: Expected hash value + algorithm: Hash algorithm used + + Returns: + True if file matches expected hash, False otherwise + """ + actual_hash = self.get_file_hash(file_path, algorithm) + return actual_hash is not None and actual_hash.lower() == expected_hash.lower() + + def get_directory_size(self, directory: Path) -> int: + """ + Calculate total size of directory in bytes + + Args: + directory: Directory path + + Returns: + Total size in bytes + """ + if not directory.exists() or not directory.is_dir(): + return 0 + + total_size = 0 + try: + for file_path in directory.rglob('*'): + if file_path.is_file(): + total_size += file_path.stat().st_size + except Exception: + pass # Skip files we can't access + + return total_size + + def find_files(self, directory: Path, pattern: str = '*', recursive: bool = True) -> List[Path]: + """ + Find files matching pattern + + Args: + directory: Directory to search + pattern: Glob pattern to match + recursive: Whether to search recursively + + Returns: + List of matching file paths + """ + if not directory.exists() or not directory.is_dir(): + return [] + + try: + if recursive: + return list(directory.rglob(pattern)) + else: + return list(directory.glob(pattern)) + except Exception: + return [] + + def backup_file(self, file_path: Path, backup_suffix: str = '.backup') -> Optional[Path]: + """ + Create backup copy of file + + Args: + file_path: Path to file to backup + backup_suffix: Suffix to add to backup file + + Returns: + Path to backup file or None if failed + """ + if not file_path.exists() or not file_path.is_file(): + return None + + backup_path = file_path.with_suffix(file_path.suffix + backup_suffix) + + if self.copy_file(file_path, backup_path): + return backup_path + return None + + def get_free_space(self, path: Path) -> int: + """ + Get free disk space at path in bytes + + Args: + path: Path to check (can be file or directory) + + Returns: + Free space in bytes + """ + try: + if path.is_file(): + path = path.parent + + stat_result = shutil.disk_usage(path) + return stat_result.free + except Exception: + return 0 + + def cleanup_tracked_files(self) -> None: + """Remove all files and directories created during this session""" + if self.dry_run: + print("[DRY RUN] Would cleanup tracked files") + return + + # Remove files first + for file_path in reversed(self.copied_files): + try: + if file_path.exists(): + file_path.unlink() + except Exception: + pass + + # Remove directories (in reverse order of creation) + for directory in reversed(self.created_dirs): + try: + if directory.exists() and not any(directory.iterdir()): + directory.rmdir() + except Exception: + pass + + self.copied_files.clear() + self.created_dirs.clear() + + def get_operation_summary(self) -> Dict[str, Any]: + """ + Get summary of file operations performed + + Returns: + Dict with operation statistics + """ + return { + 'files_copied': len(self.copied_files), + 'directories_created': len(self.created_dirs), + 'dry_run': self.dry_run, + 'copied_files': [str(f) for f in self.copied_files], + 'created_directories': [str(d) for d in self.created_dirs] + } \ No newline at end of file diff --git a/setup/managers/settings_manager.py b/setup/managers/settings_manager.py new file mode 100644 index 0000000..a85c71e --- /dev/null +++ b/setup/managers/settings_manager.py @@ -0,0 +1,515 @@ +""" +Settings management for SuperClaude installation system +Handles settings.json migration to the new SuperClaude metadata json file +Allows for manipulation of these json files with deep merge and backup +""" + +import json +import shutil +from typing import Dict, Any, Optional, List +from pathlib import Path +from datetime import datetime +import copy + + +class SettingsManager: + """Manages settings.json file operations""" + + def __init__(self, install_dir: Path): + """ + Initialize settings manager + + Args: + install_dir: Installation directory containing settings.json + """ + self.install_dir = install_dir + self.settings_file = install_dir / "settings.json" + self.metadata_file = install_dir / ".superclaude-metadata.json" + self.backup_dir = install_dir / "backups" / "settings" + + def load_settings(self) -> Dict[str, Any]: + """ + Load settings from settings.json + + Returns: + Settings dict (empty if file doesn't exist) + """ + if not self.settings_file.exists(): + return {} + + try: + with open(self.settings_file, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + raise ValueError(f"Could not load settings from {self.settings_file}: {e}") + + def save_settings(self, settings: Dict[str, Any], create_backup: bool = True) -> None: + """ + Save settings to settings.json with optional backup + + Args: + settings: Settings dict to save + create_backup: Whether to create backup before saving + """ + # Create backup if requested and file exists + if create_backup and self.settings_file.exists(): + self._create_settings_backup() + + # Ensure directory exists + self.settings_file.parent.mkdir(parents=True, exist_ok=True) + + # Save with pretty formatting + try: + with open(self.settings_file, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=2, ensure_ascii=False, sort_keys=True) + except IOError as e: + raise ValueError(f"Could not save settings to {self.settings_file}: {e}") + + def load_metadata(self) -> Dict[str, Any]: + """ + Load SuperClaude metadata from .superclaude-metadata.json + + Returns: + Metadata dict (empty if file doesn't exist) + """ + if not self.metadata_file.exists(): + return {} + + try: + with open(self.metadata_file, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + raise ValueError(f"Could not load metadata from {self.metadata_file}: {e}") + + def save_metadata(self, metadata: Dict[str, Any]) -> None: + """ + Save SuperClaude metadata to .superclaude-metadata.json + + Args: + metadata: Metadata dict to save + """ + # Ensure directory exists + self.metadata_file.parent.mkdir(parents=True, exist_ok=True) + + # Save with pretty formatting + try: + with open(self.metadata_file, 'w', encoding='utf-8') as f: + json.dump(metadata, f, indent=2, ensure_ascii=False, sort_keys=True) + except IOError as e: + raise ValueError(f"Could not save metadata to {self.metadata_file}: {e}") + + def merge_metadata(self, modifications: Dict[str, Any]) -> Dict[str, Any]: + """ + Deep merge modifications into existing settings + + Args: + modifications: Settings modifications to merge + + Returns: + Merged settings dict + """ + existing = self.load_metadata() + return self._deep_merge(existing, modifications) + + def update_metadata(self, modifications: Dict[str, Any]) -> None: + """ + Update settings with modifications + + Args: + modifications: Settings modifications to apply + create_backup: Whether to create backup before updating + """ + merged = self.merge_metadata(modifications) + self.save_metadata(merged) + + def migrate_superclaude_data(self) -> bool: + """ + Migrate SuperClaude-specific data from settings.json to metadata file + + Returns: + True if migration occurred, False if no data to migrate + """ + settings = self.load_settings() + + # SuperClaude-specific fields to migrate + superclaude_fields = ["components", "framework", "superclaude", "mcp"] + data_to_migrate = {} + fields_found = False + + # Extract SuperClaude data + for field in superclaude_fields: + if field in settings: + data_to_migrate[field] = settings[field] + fields_found = True + + if not fields_found: + return False + + # Load existing metadata (if any) and merge + existing_metadata = self.load_metadata() + merged_metadata = self._deep_merge(existing_metadata, data_to_migrate) + + # Save to metadata file + self.save_metadata(merged_metadata) + + # Remove SuperClaude fields from settings + clean_settings = {k: v for k, v in settings.items() if k not in superclaude_fields} + + # Save cleaned settings + self.save_settings(clean_settings, create_backup=True) + + return True + + def merge_settings(self, modifications: Dict[str, Any]) -> Dict[str, Any]: + """ + Deep merge modifications into existing settings + + Args: + modifications: Settings modifications to merge + + Returns: + Merged settings dict + """ + existing = self.load_settings() + return self._deep_merge(existing, modifications) + + def update_settings(self, modifications: Dict[str, Any], create_backup: bool = True) -> None: + """ + Update settings with modifications + + Args: + modifications: Settings modifications to apply + create_backup: Whether to create backup before updating + """ + merged = self.merge_settings(modifications) + self.save_settings(merged, create_backup) + + def get_setting(self, key_path: str, default: Any = None) -> Any: + """ + Get setting value using dot-notation path + + Args: + key_path: Dot-separated path (e.g., "hooks.enabled") + default: Default value if key not found + + Returns: + Setting value or default + """ + settings = self.load_settings() + + try: + value = settings + for key in key_path.split('.'): + value = value[key] + return value + except (KeyError, TypeError): + return default + + def set_setting(self, key_path: str, value: Any, create_backup: bool = True) -> None: + """ + Set setting value using dot-notation path + + Args: + key_path: Dot-separated path (e.g., "hooks.enabled") + value: Value to set + create_backup: Whether to create backup before updating + """ + # Build nested dict structure + keys = key_path.split('.') + modification = {} + current = modification + + for key in keys[:-1]: + current[key] = {} + current = current[key] + + current[keys[-1]] = value + + self.update_settings(modification, create_backup) + + def remove_setting(self, key_path: str, create_backup: bool = True) -> bool: + """ + Remove setting using dot-notation path + + Args: + key_path: Dot-separated path to remove + create_backup: Whether to create backup before updating + + Returns: + True if setting was removed, False if not found + """ + settings = self.load_settings() + keys = key_path.split('.') + + # Navigate to parent of target key + current = settings + try: + for key in keys[:-1]: + current = current[key] + + # Remove the target key + if keys[-1] in current: + del current[keys[-1]] + self.save_settings(settings, create_backup) + return True + else: + return False + + except (KeyError, TypeError): + return False + + def add_component_registration(self, component_name: str, component_info: Dict[str, Any]) -> None: + """ + Add component to registry in metadata + + Args: + component_name: Name of component + component_info: Component metadata dict + """ + metadata = self.load_metadata() + if "components" not in metadata: + metadata["components"] = {} + + metadata["components"][component_name] = { + **component_info, + "installed_at": datetime.now().isoformat() + } + + self.save_metadata(metadata) + + def remove_component_registration(self, component_name: str) -> bool: + """ + Remove component from registry in metadata + + Args: + component_name: Name of component to remove + + Returns: + True if component was removed, False if not found + """ + metadata = self.load_metadata() + if "components" in metadata and component_name in metadata["components"]: + del metadata["components"][component_name] + self.save_metadata(metadata) + return True + return False + + def get_installed_components(self) -> Dict[str, Dict[str, Any]]: + """ + Get all installed components from registry + + Returns: + Dict of component_name -> component_info + """ + metadata = self.load_metadata() + return metadata.get("components", {}) + + def is_component_installed(self, component_name: str) -> bool: + """ + Check if component is registered as installed + + Args: + component_name: Name of component to check + + Returns: + True if component is installed, False otherwise + """ + components = self.get_installed_components() + return component_name in components + + def get_component_version(self, component_name: str) -> Optional[str]: + """ + Get installed version of component + + Args: + component_name: Name of component + + Returns: + Version string or None if not installed + """ + components = self.get_installed_components() + component_info = components.get(component_name, {}) + return component_info.get("version") + + def update_framework_version(self, version: str) -> None: + """ + Update SuperClaude framework version in metadata + + Args: + version: Framework version string + """ + metadata = self.load_metadata() + if "framework" not in metadata: + metadata["framework"] = {} + + metadata["framework"]["version"] = version + metadata["framework"]["updated_at"] = datetime.now().isoformat() + + self.save_metadata(metadata) + + def check_installation_exists(self) -> bool: + """ + Get SuperClaude framework version from metadata + + Returns: + Version string or None if not set + """ + return self.metadata_file.exists() + + def check_v2_installation_exists(self) -> bool: + """ + Get SuperClaude framework version from metadata + + Returns: + Version string or None if not set + """ + return self.settings_file.exists() + + def get_metadata_setting(self, key_path: str, default: Any = None) -> Any: + """ + Get metadata value using dot-notation path + + Args: + key_path: Dot-separated path (e.g., "framework.version") + default: Default value if key not found + + Returns: + Metadata value or default + """ + metadata = self.load_metadata() + + try: + value = metadata + for key in key_path.split('.'): + value = value[key] + return value + except (KeyError, TypeError): + return default + + def _deep_merge(self, base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]: + """ + Deep merge two dictionaries + + Args: + base: Base dictionary + overlay: Dictionary to merge on top + + Returns: + Merged dictionary + """ + result = copy.deepcopy(base) + + for key, value in overlay.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = self._deep_merge(result[key], value) + else: + result[key] = copy.deepcopy(value) + + return result + + def _create_settings_backup(self) -> Path: + """ + Create timestamped backup of settings.json + + Returns: + Path to backup file + """ + if not self.settings_file.exists(): + raise ValueError("Cannot backup non-existent settings file") + + # Create backup directory + self.backup_dir.mkdir(parents=True, exist_ok=True) + + # Create timestamped backup + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = self.backup_dir / f"settings_{timestamp}.json" + + shutil.copy2(self.settings_file, backup_file) + + # Keep only last 10 backups + self._cleanup_old_backups() + + return backup_file + + def _cleanup_old_backups(self, keep_count: int = 10) -> None: + """ + Remove old backup files, keeping only the most recent + + Args: + keep_count: Number of backups to keep + """ + if not self.backup_dir.exists(): + return + + # Get all backup files sorted by modification time + backup_files = [] + for file in self.backup_dir.glob("settings_*.json"): + backup_files.append((file.stat().st_mtime, file)) + + backup_files.sort(reverse=True) # Most recent first + + # Remove old backups + for _, file in backup_files[keep_count:]: + try: + file.unlink() + except OSError: + pass # Ignore errors when cleaning up + + def list_backups(self) -> List[Dict[str, Any]]: + """ + List available settings backups + + Returns: + List of backup info dicts with name, path, and timestamp + """ + if not self.backup_dir.exists(): + return [] + + backups = [] + for file in self.backup_dir.glob("settings_*.json"): + try: + stat = file.stat() + backups.append({ + "name": file.name, + "path": str(file), + "size": stat.st_size, + "created": datetime.fromtimestamp(stat.st_ctime).isoformat(), + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + }) + except OSError: + continue + + # Sort by creation time, most recent first + backups.sort(key=lambda x: x["created"], reverse=True) + return backups + + def restore_backup(self, backup_name: str) -> bool: + """ + Restore settings from backup + + Args: + backup_name: Name of backup file to restore + + Returns: + True if successful, False otherwise + """ + backup_file = self.backup_dir / backup_name + + if not backup_file.exists(): + return False + + try: + # Validate backup file first + with open(backup_file, 'r', encoding='utf-8') as f: + json.load(f) # Will raise exception if invalid + + # Create backup of current settings + if self.settings_file.exists(): + self._create_settings_backup() + + # Restore backup + shutil.copy2(backup_file, self.settings_file) + return True + + except (json.JSONDecodeError, IOError): + return False diff --git a/setup/operations/__init__.py b/setup/operations/__init__.py new file mode 100644 index 0000000..589f3e0 --- /dev/null +++ b/setup/operations/__init__.py @@ -0,0 +1,85 @@ +""" +SuperClaude Operations Module + +This module contains all SuperClaude management operations that can be +executed through the unified CLI hub (SuperClaude). + +Each operation module should implement: +- register_parser(subparsers): Register CLI arguments for the operation +- run(args): Execute the operation with parsed arguments + +Available operations: +- install: Install SuperClaude framework components +- update: Update existing SuperClaude installation +- uninstall: Remove SuperClaude framework installation +- backup: Backup and restore SuperClaude installations +""" + +__version__ = "3.0.0" +__all__ = ["install", "update", "uninstall", "backup"] + + +def get_operation_info(): + """Get information about available operations""" + return { + "install": { + "name": "install", + "description": "Install SuperClaude framework components", + "module": "setup.operations.install" + }, + "update": { + "name": "update", + "description": "Update existing SuperClaude installation", + "module": "setup.operations.update" + }, + "uninstall": { + "name": "uninstall", + "description": "Remove SuperClaude framework installation", + "module": "setup.operations.uninstall" + }, + "backup": { + "name": "backup", + "description": "Backup and restore SuperClaude installations", + "module": "setup.operations.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: + from ..utils.security import SecurityValidator + 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 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 \ No newline at end of file diff --git a/setup/operations/backup.py b/setup/operations/backup.py new file mode 100644 index 0000000..e6d1f30 --- /dev/null +++ b/setup/operations/backup.py @@ -0,0 +1,589 @@ +""" +SuperClaude Backup Operation Module +Refactored from backup.py for unified CLI hub +""" + +import sys +import time +import tarfile +import json +from pathlib import Path +from datetime import datetime +from typing import List, Optional, Dict, Any, Tuple +import argparse + +from ..managers.settings_manager import SettingsManager +from ..utils.ui import ( + 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 +from . import OperationBase + + +class BackupOperation(OperationBase): + """Backup operation implementation""" + + def __init__(self): + super().__init__("backup") + + +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", + description="Create, list, restore, and manage SuperClaude installation backups", + epilog=""" +Examples: + SuperClaude backup --create # Create new backup + SuperClaude backup --list --verbose # List available backups (verbose) + SuperClaude backup --restore # Interactive restore + SuperClaude backup --restore backup.tar.gz # Restore specific backup + SuperClaude backup --info backup.tar.gz # Show backup information + SuperClaude backup --cleanup --force # Clean up old backups (forced) + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + 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" + ) + + operation_group.add_argument( + "--list", + action="store_true", + help="List available backups" + ) + + operation_group.add_argument( + "--restore", + nargs="?", + const="interactive", + help="Restore from backup (optionally specify backup file)" + ) + + operation_group.add_argument( + "--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" + ) + + # Backup options + parser.add_argument( + "--backup-dir", + type=Path, + help="Backup directory (default: /backups)" + ) + + 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)" + ) + + # Restore options + parser.add_argument( + "--overwrite", + action="store_true", + 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)" + ) + + parser.add_argument( + "--older-than", + type=int, + help="Remove backups older than N days" + ) + + return parser + + +def get_backup_directory(args: argparse.Namespace) -> Path: + """Get the backup directory path""" + if args.backup_dir: + return args.backup_dir + else: + return args.install_dir / "backups" + + +def check_installation_exists(install_dir: Path) -> bool: + """Check if SuperClaude installation (v2 included) exists""" + settings_manager = SettingsManager(install_dir) + + return settings_manager.check_installation_exists() or settings_manager.check_v2_installation_exists() + + +def get_backup_info(backup_path: Path) -> Dict[str, Any]: + """Get information about a backup file""" + info = { + "path": backup_path, + "exists": backup_path.exists(), + "size": 0, + "created": None, + "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" + elif backup_path.suffix == ".bz2": + mode = "r:bz2" + else: + mode = "r" + + with tarfile.open(backup_path, mode) as tar: + # Look for metadata file + try: + metadata_member = tar.getmember("backup_metadata.json") + metadata_file = tar.extractfile(metadata_member) + if metadata_file: + 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 + + +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" + 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""" + metadata = { + "backup_version": "3.0.0", + "created": datetime.now().isoformat(), + "install_dir": str(install_dir), + "components": {}, + "framework_version": "unknown" + } + + try: + # Get installed components from metadata + settings_manager = SettingsManager(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) + if version: + 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" + mode = "w:gz" + elif args.compress == "bzip2": + backup_file = backup_dir / f"{backup_name}.tar.bz2" + mode = "w:bz2" + 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: + 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 + files_added = 0 + for item in args.install_dir.rglob("*"): + if item.is_file() and item != backup_file: + try: + # Create relative path for archive + rel_path = item.relative_to(args.install_dir) + 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 + + +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" + elif backup_path.suffix == ".bz2": + 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 + + +def interactive_restore_selection(backups: List[Dict[str, Any]]) -> Optional[Path]: + """Interactive backup selection for restore""" + 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" + 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:]) + + # 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 + + +def run(args: argparse.Namespace) -> int: + """Execute backup operation with parsed arguments""" + operation = BackupOperation() + operation.setup_operation_logging(args) + logger = get_logger() + # ✅ Inserted validation code + expected_home = Path.home().resolve() + actual_dir = args.install_dir.resolve() + + if not str(actual_dir).startswith(str(expected_home)): + print(f"\n[✗] Installation must be inside your user profile directory.") + 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) + if not success: + for error in errors: + logger.error(error) + return 1 + + # Display header + if not args.quiet: + display_header( + "SuperClaude Backup v3.0", + "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 + backups = list_backups(backup_dir) + backup_path = interactive_restore_selection(backups) + if not backup_path: + logger.info("Restore cancelled by user") + return 0 + else: + # Specific backup file + 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}") + print(f"File: {info['path']}") + 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')}") + if metadata.get("components"): + print("Components:") + for comp, ver in metadata["components"].items(): + print(f" {comp}: v{ver}") + else: + 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!") + elif not args.quiet and args.restore: + display_success("Restore operation completed successfully!") + return 0 + 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 + except Exception as e: + return operation.handle_operation_error("backup", e) diff --git a/setup/operations/install.py b/setup/operations/install.py new file mode 100644 index 0000000..97d5eec --- /dev/null +++ b/setup/operations/install.py @@ -0,0 +1,542 @@ +""" +SuperClaude Installation Operation Module +Refactored from install.py for unified CLI hub +""" + +import sys +import time +from pathlib import Path +from typing import List, Optional, Dict, Any +import argparse + +from ..base.installer import Installer +from ..core.registry import ComponentRegistry +from ..managers.config_manager import ConfigManager +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 +) +from ..utils.logger import get_logger +from .. import DEFAULT_INSTALL_DIR, PROJECT_ROOT +from . import OperationBase + + +class InstallOperation(OperationBase): + """Installation operation implementation""" + + def __init__(self): + super().__init__("install") + + +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", + description="Install SuperClaude Framework with various options and profiles", + epilog=""" +Examples: + SuperClaude install # Interactive installation + SuperClaude install --quick --dry-run # Quick installation (dry-run) + SuperClaude install --profile developer # Developer profile + SuperClaude install --components core mcp # Specific components + SuperClaude install --verbose --force # Verbose with force mode + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=parents + ) + + # Installation mode options + parser.add_argument( + "--quick", + action="store_true", + help="Quick installation with pre-selected components" + ) + + parser.add_argument( + "--minimal", + action="store_true", + help="Minimal installation (core only)" + ) + + parser.add_argument( + "--profile", + type=str, + help="Installation profile (quick, minimal, developer, etc.)" + ) + + parser.add_argument( + "--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( + "--list-components", + action="store_true", + help="List available components and exit" + ) + + parser.add_argument( + "--diagnose", + action="store_true", + help="Run system diagnostics and show installation help" + ) + + return parser + + +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 = ConfigManager(PROJECT_ROOT / "config") + requirements = config_manager.get_requirements_for_components(component_names) + + # Validate requirements + success, errors = validator.validate_component_requirements(component_names, requirements) + + if success: + logger.success("All system requirements met") + return True + else: + 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(" 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: ConfigManager) -> Optional[List[str]]: + """Determine which components to install""" + logger = get_logger() + + # Explicit components specified + if args.components: + if 'all' in args.components: + return ["core", "commands", "hooks", "mcp"] + return args.components + + # Profile-based selection + if args.profile: + try: + profile_path = PROJECT_ROOT / "profiles" / f"{args.profile}.json" + profile = config_manager.load_profile(profile_path) + return profile["components"] + except Exception as e: + logger.error(f"Could not load profile '{args.profile}': {e}") + return None + + # Quick installation + if args.quick: + try: + profile_path = PROJECT_ROOT / "profiles" / "quick.json" + profile = config_manager.load_profile(profile_path) + return profile["components"] + except Exception as e: + logger.warning(f"Could not load quick profile: {e}") + return ["core"] # Fallback to core only + + # Minimal installation + if args.minimal: + return ["core"] + + # Interactive selection + return interactive_component_selection(registry, config_manager) + + +def interactive_component_selection(registry: ComponentRegistry, config_manager: ConfigManager) -> Optional[List[str]]: + """Interactive component selection""" + logger = get_logger() + + try: + # Get available components + available_components = registry.list_components() + + if not available_components: + logger.error("No components available for installation") + return None + + # Create component menu with descriptions + menu_options = [] + component_info = {} + + for component_name in available_components: + metadata = registry.get_component_metadata(component_name) + if metadata: + description = metadata.get("description", "No description") + category = metadata.get("category", "unknown") + menu_options.append(f"{component_name} ({category}) - {description}") + component_info[component_name] = metadata + else: + menu_options.append(f"{component_name} - Component description unavailable") + component_info[component_name] = {"description": "Unknown"} + + # Add preset options + preset_options = [ + "Quick Installation (recommended components)", + "Minimal Installation (core only)", + "Custom Selection" + ] + + print(f"\n{Colors.CYAN}SuperClaude Installation Options:{Colors.RESET}") + menu = Menu("Select installation type:", preset_options) + choice = menu.display() + + if choice == -1: # Cancelled + return None + elif choice == 0: # Quick + try: + profile_path = PROJECT_ROOT / "profiles" / "quick.json" + profile = config_manager.load_profile(profile_path) + return profile["components"] + except Exception: + return ["core"] + elif choice == 1: # Minimal + return ["core"] + elif choice == 2: # Custom + print(f"\n{Colors.CYAN}Available Components:{Colors.RESET}") + component_menu = Menu("Select components to install:", menu_options, multi_select=True) + selections = component_menu.display() + + if not selections: + logger.warning("No components selected") + return None + + return [available_components[i] for i in selections] + + return None + + 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: + """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'): + 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() + + except Exception as e: + logger.error(f"Could not resolve dependencies: {e}") + raise + + +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': + print(f" ✅ {check_name}: {message}") + else: + print(f" ❌ {check_name}: {message}") + all_passed = False + + # Display issues and recommendations + if diagnostics['issues']: + print(f"\n{Colors.YELLOW}Issues Found:{Colors.RESET}") + for issue in diagnostics['issues']: + print(f" ⚠️ {issue}") + + print(f"\n{Colors.CYAN}Recommendations:{Colors.RESET}") + 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}") + else: + 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)") + 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") + + +def perform_installation(components: List[str], args: argparse.Namespace) -> 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) + + if not component_instances: + logger.error("No valid component instances created") + return False + + # Register components with installer + installer.register_components(list(component_instances.values())) + + # Resolve dependencies + ordered_components = registry.resolve_dependencies(components) + + # Setup progress tracking + progress = ProgressBar( + 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 + } + + success = installer.install_components(ordered_components, config) + + # Update progress + for i, component_name in enumerate(ordered_components): + if component_name in installer.installed_components: + progress.update(i + 1, f"Installed {component_name}") + 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") + + # Show summary + summary = installer.get_installation_summary() + if summary['installed']: + logger.info(f"Installed components: {', '.join(summary['installed'])}") + + if summary['backup_path']: + logger.info(f"Backup created: {summary['backup_path']}") + + else: + logger.error(f"Installation completed with errors in {duration:.1f} seconds") + + summary = installer.get_installation_summary() + 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 + + +def run(args: argparse.Namespace) -> int: + """Execute installation operation with parsed arguments""" + operation = InstallOperation() + operation.setup_operation_logging(args) + logger = get_logger() + # ✅ Inserted validation code + expected_home = Path.home().resolve() + actual_dir = args.install_dir.resolve() + + if not str(actual_dir).startswith(str(expected_home)): + print(f"\n[✗] Installation must be inside your user profile directory.") + 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) + if not success: + for error in errors: + logger.error(error) + return 1 + + # Display header + if not args.quiet: + display_header( + "SuperClaude Installation v3.0", + "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}") + for component_name in components: + metadata = registry.get_component_metadata(component_name) + if metadata: + desc = metadata.get("description", "No description") + category = metadata.get("category", "unknown") + print(f" {component_name} ({category}) - {desc}") + else: + print(f" {component_name} - Unknown component") + 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 = ConfigManager(PROJECT_ROOT / "config") + validator = Validator() + + # Validate configuration + config_errors = config_manager.validate_config_files() + if config_errors: + logger.error("Configuration validation failed:") + for error in config_errors: + logger.error(f" - {error}") + return 1 + + # Get components to install + components = get_components_to_install(args, registry, config_manager) + if not components: + logger.error("No components selected for installation") + return 1 + + # Validate system requirements + if not validate_system_requirements(validator, 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") + + # 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.info("Installation cancelled by user") + return 0 + + # Display installation plan + if not args.quiet: + display_installation_plan(components, registry, args.install_dir) + + if not args.dry_run: + 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(components, args) + + 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 + except Exception as e: + return operation.handle_operation_error("install", e) diff --git a/setup/operations/uninstall.py b/setup/operations/uninstall.py new file mode 100644 index 0000000..dda53cb --- /dev/null +++ b/setup/operations/uninstall.py @@ -0,0 +1,490 @@ +""" +SuperClaude Uninstall Operation Module +Refactored from uninstall.py for unified CLI hub +""" + +import sys +import time +from pathlib import Path +from typing import List, Optional, Dict, Any +import argparse + +from ..core.registry import ComponentRegistry +from ..managers.settings_manager import SettingsManager +from ..managers.file_manager import FileManager +from ..utils.ui import ( + display_header, display_info, display_success, display_error, + display_warning, Menu, confirm, ProgressBar, Colors +) +from ..utils.logger import get_logger +from .. import DEFAULT_INSTALL_DIR, PROJECT_ROOT +from . import OperationBase + + +class UninstallOperation(OperationBase): + """Uninstall operation implementation""" + + def __init__(self): + super().__init__("uninstall") + + +def register_parser(subparsers, global_parser=None) -> argparse.ArgumentParser: + """Register uninstall CLI arguments""" + parents = [global_parser] if global_parser else [] + + parser = subparsers.add_parser( + "uninstall", + help="Remove SuperClaude framework installation", + description="Uninstall SuperClaude Framework components", + epilog=""" +Examples: + SuperClaude uninstall # Interactive uninstall + SuperClaude uninstall --components core # Remove specific components + SuperClaude uninstall --complete --force # Complete removal (forced) + SuperClaude uninstall --keep-backups # Keep backup files + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=parents + ) + + # Uninstall mode options + parser.add_argument( + "--components", + type=str, + nargs="+", + help="Specific components to uninstall" + ) + + parser.add_argument( + "--complete", + action="store_true", + help="Complete uninstall (remove all files and directories)" + ) + + # Data preservation options + parser.add_argument( + "--keep-backups", + action="store_true", + help="Keep backup files during uninstall" + ) + + parser.add_argument( + "--keep-logs", + action="store_true", + help="Keep log files during uninstall" + ) + + parser.add_argument( + "--keep-settings", + action="store_true", + help="Keep user settings during uninstall" + ) + + # Safety options + parser.add_argument( + "--no-confirm", + action="store_true", + help="Skip confirmation prompts (use with caution)" + ) + + return parser + +def get_installed_components(install_dir: Path) -> Dict[str, Dict[str, Any]]: + """Get currently installed components and their versions""" + try: + settings_manager = SettingsManager(install_dir) + return settings_manager.get_installed_components() + except Exception: + return {} + + +def get_installation_info(install_dir: Path) -> Dict[str, Any]: + """Get detailed installation information""" + info = { + "install_dir": install_dir, + "exists": False, + "components": {}, + "directories": [], + "files": [], + "total_size": 0 + } + + if not install_dir.exists(): + return info + + info["exists"] = True + info["components"] = get_installed_components(install_dir) + + # Scan installation directory + try: + for item in install_dir.rglob("*"): + if item.is_file(): + info["files"].append(item) + info["total_size"] += item.stat().st_size + elif item.is_dir(): + info["directories"].append(item) + except Exception: + pass + + return info + + +def display_uninstall_info(info: Dict[str, Any]) -> None: + """Display installation information before uninstall""" + print(f"\n{Colors.CYAN}{Colors.BRIGHT}Current Installation{Colors.RESET}") + print("=" * 50) + + if not info["exists"]: + print(f"{Colors.YELLOW}No SuperClaude installation found{Colors.RESET}") + return + + print(f"{Colors.BLUE}Installation Directory:{Colors.RESET} {info['install_dir']}") + + if info["components"]: + print(f"{Colors.BLUE}Installed Components:{Colors.RESET}") + for component, version in info["components"].items(): + print(f" {component}: v{version}") + + print(f"{Colors.BLUE}Files:{Colors.RESET} {len(info['files'])}") + print(f"{Colors.BLUE}Directories:{Colors.RESET} {len(info['directories'])}") + + if info["total_size"] > 0: + from ..utils.ui import format_size + print(f"{Colors.BLUE}Total Size:{Colors.RESET} {format_size(info['total_size'])}") + + print() + + +def get_components_to_uninstall(args: argparse.Namespace, installed_components: Dict[str, str]) -> Optional[List[str]]: + """Determine which components to uninstall""" + logger = get_logger() + + # Complete uninstall + if args.complete: + return list(installed_components.keys()) + + # 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] + if invalid_components: + logger.error(f"Components not installed: {invalid_components}") + return None + return args.components + + # Interactive selection + return interactive_uninstall_selection(installed_components) + + +def interactive_uninstall_selection(installed_components: Dict[str, str]) -> Optional[List[str]]: + """Interactive uninstall selection""" + if not installed_components: + return [] + + print(f"\n{Colors.CYAN}Uninstall Options:{Colors.RESET}") + + # Create menu options + preset_options = [ + "Complete Uninstall (remove everything)", + "Remove Specific Components", + "Cancel Uninstall" + ] + + menu = Menu("Select uninstall option:", preset_options) + choice = menu.display() + + if choice == -1 or choice == 2: # Cancelled + return None + elif choice == 0: # Complete uninstall + return list(installed_components.keys()) + elif choice == 1: # Select specific components + component_options = [] + component_names = [] + + for component, version in installed_components.items(): + component_options.append(f"{component} (v{version})") + component_names.append(component) + + component_menu = Menu("Select components to uninstall:", component_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_uninstall_plan(components: List[str], args: argparse.Namespace, info: Dict[str, Any]) -> None: + """Display uninstall plan""" + print(f"\n{Colors.CYAN}{Colors.BRIGHT}Uninstall Plan{Colors.RESET}") + print("=" * 50) + + print(f"{Colors.BLUE}Installation Directory:{Colors.RESET} {info['install_dir']}") + + if components: + print(f"{Colors.BLUE}Components to remove:{Colors.RESET}") + for i, component_name in enumerate(components, 1): + version = info["components"].get(component_name, "unknown") + print(f" {i}. {component_name} (v{version})") + + # Show what will be preserved + preserved = [] + if args.keep_backups: + preserved.append("backup files") + if args.keep_logs: + preserved.append("log files") + if args.keep_settings: + preserved.append("user settings") + + if preserved: + print(f"{Colors.GREEN}Will preserve:{Colors.RESET} {', '.join(preserved)}") + + if args.complete: + print(f"{Colors.RED}WARNING: Complete uninstall will remove all SuperClaude files{Colors.RESET}") + + print() + + +def create_uninstall_backup(install_dir: Path, components: List[str]) -> Optional[Path]: + """Create backup before uninstall""" + logger = get_logger() + + try: + from datetime import datetime + backup_dir = install_dir / "backups" + backup_dir.mkdir(exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_name = f"pre_uninstall_{timestamp}.tar.gz" + backup_path = backup_dir / backup_name + + import tarfile + + logger.info(f"Creating uninstall backup: {backup_path}") + + with tarfile.open(backup_path, "w:gz") as tar: + for component in components: + # Add component files to backup + settings_manager = SettingsManager(install_dir) + # This would need component-specific backup logic + pass + + logger.success(f"Backup created: {backup_path}") + return backup_path + + except Exception as e: + logger.warning(f"Could not create backup: {e}") + return None + + +def perform_uninstall(components: List[str], args: argparse.Namespace, info: Dict[str, Any]) -> bool: + """Perform the actual uninstall""" + logger = get_logger() + start_time = time.time() + + try: + # 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) + + # Setup progress tracking + progress = ProgressBar( + total=len(components), + prefix="Uninstalling: ", + suffix="" + ) + + # Uninstall components + logger.info(f"Uninstalling {len(components)} components...") + + uninstalled_components = [] + failed_components = [] + + for i, component_name in enumerate(components): + progress.update(i, f"Uninstalling {component_name}") + + try: + if component_name in component_instances: + instance = component_instances[component_name] + if instance.uninstall(): + uninstalled_components.append(component_name) + logger.debug(f"Successfully uninstalled {component_name}") + else: + failed_components.append(component_name) + logger.error(f"Failed to uninstall {component_name}") + else: + logger.warning(f"Component {component_name} not found, skipping") + + except Exception as e: + logger.error(f"Error uninstalling {component_name}: {e}") + failed_components.append(component_name) + + progress.update(i + 1, f"Processed {component_name}") + time.sleep(0.1) # Brief pause for visual effect + + progress.finish("Uninstall complete") + + # Handle complete uninstall cleanup + if args.complete: + cleanup_installation_directory(args.install_dir, args) + + # Show results + duration = time.time() - start_time + + if failed_components: + logger.warning(f"Uninstall completed with some failures in {duration:.1f} seconds") + logger.warning(f"Failed components: {', '.join(failed_components)}") + else: + logger.success(f"Uninstall completed successfully in {duration:.1f} seconds") + + if uninstalled_components: + logger.info(f"Uninstalled components: {', '.join(uninstalled_components)}") + + return len(failed_components) == 0 + + except Exception as e: + logger.exception(f"Unexpected error during uninstall: {e}") + return False + + +def cleanup_installation_directory(install_dir: Path, args: argparse.Namespace) -> None: + """Clean up installation directory for complete uninstall""" + logger = get_logger() + file_manager = FileManager() + + try: + # Preserve specific directories/files if requested + preserve_patterns = [] + + if args.keep_backups: + preserve_patterns.append("backups/*") + if args.keep_logs: + preserve_patterns.append("logs/*") + if args.keep_settings and not args.complete: + preserve_patterns.append("settings.json") + + # Remove installation directory contents + if args.complete and not preserve_patterns: + # Complete removal + if file_manager.remove_directory(install_dir): + logger.info(f"Removed installation directory: {install_dir}") + else: + logger.warning(f"Could not remove installation directory: {install_dir}") + else: + # Selective removal + for item in install_dir.iterdir(): + should_preserve = False + + for pattern in preserve_patterns: + if item.match(pattern): + should_preserve = True + break + + if not should_preserve: + if item.is_file(): + file_manager.remove_file(item) + elif item.is_dir(): + file_manager.remove_directory(item) + + except Exception as e: + logger.error(f"Error during cleanup: {e}") + + +def run(args: argparse.Namespace) -> int: + """Execute uninstall operation with parsed arguments""" + operation = UninstallOperation() + operation.setup_operation_logging(args) + logger = get_logger() + # ✅ Inserted validation code + expected_home = Path.home().resolve() + actual_dir = args.install_dir.resolve() + + if not str(actual_dir).startswith(str(expected_home)): + print(f"\n[✗] Installation must be inside your user profile directory.") + 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) + if not success: + for error in errors: + logger.error(error) + return 1 + + # Display header + if not args.quiet: + display_header( + "SuperClaude Uninstall v3.0", + "Removing SuperClaude framework components" + ) + + # Get installation information + info = get_installation_info(args.install_dir) + + # Display current installation + if not args.quiet: + display_uninstall_info(info) + + # Check if SuperClaude is installed + if not info["exists"]: + logger.warning(f"No SuperClaude installation found in {args.install_dir}") + return 0 + + # Get components to uninstall + components = get_components_to_uninstall(args, info["components"]) + if components is None: + logger.info("Uninstall cancelled by user") + return 0 + elif not components: + logger.info("No components selected for uninstall") + return 0 + + # Display uninstall plan + if not args.quiet: + display_uninstall_plan(components, args, info) + + # Confirmation + if not args.no_confirm and not args.yes: + if args.complete: + warning_msg = "This will completely remove SuperClaude. Continue?" + else: + warning_msg = f"This will remove {len(components)} component(s). Continue?" + + if not confirm(warning_msg, default=False): + logger.info("Uninstall cancelled by user") + return 0 + + # Create backup if not dry run and not keeping backups + if not args.dry_run and not args.keep_backups: + create_uninstall_backup(args.install_dir, components) + + # Perform uninstall + success = perform_uninstall(components, args, info) + + if success: + if not args.quiet: + display_success("SuperClaude uninstall completed successfully!") + + if not args.dry_run: + print(f"\n{Colors.CYAN}Uninstall complete:{Colors.RESET}") + print(f"SuperClaude has been removed from {args.install_dir}") + if not args.complete: + print(f"You can reinstall anytime using 'SuperClaude install'") + + return 0 + else: + display_error("Uninstall completed with some failures. Check logs for details.") + return 1 + + except KeyboardInterrupt: + print(f"\n{Colors.YELLOW}Uninstall cancelled by user{Colors.RESET}") + return 130 + except Exception as e: + return operation.handle_operation_error("uninstall", e) diff --git a/setup/operations/update.py b/setup/operations/update.py new file mode 100644 index 0000000..3aeb827 --- /dev/null +++ b/setup/operations/update.py @@ -0,0 +1,420 @@ +""" +SuperClaude Update Operation Module +Refactored from update.py for unified CLI hub +""" + +import sys +import time +from pathlib import Path +from typing import List, Optional, Dict, Any +import argparse + +from ..base.installer import Installer +from ..core.registry import ComponentRegistry +from ..managers.settings_manager import SettingsManager +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 +) +from ..utils.logger import get_logger +from .. import DEFAULT_INSTALL_DIR, PROJECT_ROOT +from . import OperationBase + + +class UpdateOperation(OperationBase): + """Update operation implementation""" + + def __init__(self): + super().__init__("update") + + +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", + description="Update SuperClaude Framework components to latest versions", + epilog=""" +Examples: + SuperClaude update # Interactive update + SuperClaude update --check --verbose # Check for updates (verbose) + SuperClaude update --components core mcp # Update specific components + SuperClaude update --backup --force # Create backup before update (forced) + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=parents + ) + + # Update mode options + parser.add_argument( + "--check", + action="store_true", + help="Check for available updates without installing" + ) + + parser.add_argument( + "--components", + type=str, + nargs="+", + help="Specific components to update" + ) + + # Backup options + parser.add_argument( + "--backup", + action="store_true", + help="Create backup before update" + ) + + 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" + ) + + return parser + +def check_installation_exists(install_dir: Path) -> bool: + """Check if SuperClaude installation exists""" + settings_manager = SettingsManager(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: + settings_manager = SettingsManager(install_dir) + return settings_manager.get_installed_components() + except Exception: + return {} + + +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) + if metadata: + available_version = metadata.get("version", "unknown") + if available_version != current_version: + updates[component_name] = { + "current": current_version, + "available": available_version, + "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: + """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(): + print(f" {component}: v{info['current']} → v{info['available']}") + 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]]: + """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] + 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 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" + ] + + 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) + 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: + """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] + print(f" {i}. {component_name}: v{info['current']} → v{info['available']}") + 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) -> 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 registry + registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components") + registry.discover_components() + + # Create component instances + 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())) + + # Setup progress tracking + 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 + } + + success = installer.update_components(components, config) + + # Update progress + for i, component_name in enumerate(components): + if component_name in installer.updated_components: + progress.update(i + 1, f"Updated {component_name}") + 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'): + logger.info(f"Updated components: {', '.join(summary['updated'])}") + + 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'): + logger.error(f"Failed components: {', '.join(summary['failed'])}") + + return success + + except Exception as e: + logger.exception(f"Unexpected error during update: {e}") + return False + + +def run(args: argparse.Namespace) -> int: + """Execute update operation with parsed arguments""" + operation = UpdateOperation() + operation.setup_operation_logging(args) + logger = get_logger() + # ✅ Inserted validation code + expected_home = Path.home().resolve() + actual_dir = args.install_dir.resolve() + + if not str(actual_dir).startswith(str(expected_home)): + print(f"\n[✗] Installation must be inside your user profile directory.") + 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) + if not success: + for error in errors: + logger.error(error) + return 1 + + # Display header + if not args.quiet: + display_header( + "SuperClaude Update v3.0", + "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") + 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) + 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) + + 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) + + 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 + except Exception as e: + return operation.handle_operation_error("update", e) diff --git a/setup/utils/__init__.py b/setup/utils/__init__.py new file mode 100644 index 0000000..22f1cc1 --- /dev/null +++ b/setup/utils/__init__.py @@ -0,0 +1,14 @@ +"""Utility modules for SuperClaude installation system""" + +from .ui import ProgressBar, Menu, confirm, Colors +from .logger import Logger +from .security import SecurityValidator + +__all__ = [ + 'ProgressBar', + 'Menu', + 'confirm', + 'Colors', + 'Logger', + 'SecurityValidator' +] \ No newline at end of file diff --git a/setup/utils/logger.py b/setup/utils/logger.py new file mode 100644 index 0000000..d548f4b --- /dev/null +++ b/setup/utils/logger.py @@ -0,0 +1,330 @@ +""" +Logging system for SuperClaude installation suite +""" + +import logging +import sys +from datetime import datetime +from pathlib import Path +from typing import Optional, Dict, Any +from enum import Enum + +from .ui import Colors + + +class LogLevel(Enum): + """Log levels""" + DEBUG = logging.DEBUG + INFO = logging.INFO + WARNING = logging.WARNING + ERROR = logging.ERROR + CRITICAL = logging.CRITICAL + + +class Logger: + """Enhanced logger with console and file output""" + + def __init__(self, name: str = "superclaude", log_dir: Optional[Path] = None, console_level: LogLevel = LogLevel.INFO, file_level: LogLevel = LogLevel.DEBUG): + """ + Initialize logger + + Args: + name: Logger name + log_dir: Directory for log files (defaults to ~/.claude/logs) + console_level: Minimum level for console output + file_level: Minimum level for file output + """ + self.name = name + self.log_dir = log_dir or (Path.home() / ".claude" / "logs") + self.console_level = console_level + self.file_level = file_level + self.session_start = datetime.now() + + # Create logger + self.logger = logging.getLogger(name) + self.logger.setLevel(logging.DEBUG) # Accept all levels, handlers will filter + + # Remove existing handlers to avoid duplicates + self.logger.handlers.clear() + + # Setup handlers + self._setup_console_handler() + self._setup_file_handler() + + self.log_counts: Dict[str, int] = { + 'debug': 0, + 'info': 0, + 'warning': 0, + 'error': 0, + 'critical': 0 + } + + def _setup_console_handler(self) -> None: + """Setup colorized console handler""" + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(self.console_level.value) + + # Custom formatter with colors + class ColorFormatter(logging.Formatter): + def format(self, record): + # Color mapping + colors = { + 'DEBUG': Colors.WHITE, + 'INFO': Colors.BLUE, + 'WARNING': Colors.YELLOW, + 'ERROR': Colors.RED, + 'CRITICAL': Colors.RED + Colors.BRIGHT + } + + # Prefix mapping + prefixes = { + 'DEBUG': '[DEBUG]', + 'INFO': '[INFO]', + 'WARNING': '[!]', + 'ERROR': '[✗]', + 'CRITICAL': '[CRITICAL]' + } + + color = colors.get(record.levelname, Colors.WHITE) + prefix = prefixes.get(record.levelname, '[LOG]') + + return f"{color}{prefix} {record.getMessage()}{Colors.RESET}" + + handler.setFormatter(ColorFormatter()) + self.logger.addHandler(handler) + + def _setup_file_handler(self) -> None: + """Setup file handler with rotation""" + try: + # Ensure log directory exists + self.log_dir.mkdir(parents=True, exist_ok=True) + + # Create timestamped log file + timestamp = self.session_start.strftime("%Y%m%d_%H%M%S") + log_file = self.log_dir / f"{self.name}_{timestamp}.log" + + handler = logging.FileHandler(log_file, encoding='utf-8') + handler.setLevel(self.file_level.value) + + # Detailed formatter for files + formatter = logging.Formatter( + '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + handler.setFormatter(formatter) + + self.logger.addHandler(handler) + self.log_file = log_file + + # Clean up old log files (keep last 10) + self._cleanup_old_logs() + + except Exception as e: + # If file logging fails, continue with console only + print(f"{Colors.YELLOW}[!] Could not setup file logging: {e}{Colors.RESET}") + self.log_file = None + + def _cleanup_old_logs(self, keep_count: int = 10) -> None: + """Clean up old log files""" + try: + # Get all log files for this logger + log_files = list(self.log_dir.glob(f"{self.name}_*.log")) + + # Sort by modification time, newest first + log_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) + + # Remove old files + for old_file in log_files[keep_count:]: + try: + old_file.unlink() + except OSError: + pass # Ignore errors when cleaning up + + except Exception: + pass # Ignore cleanup errors + + def debug(self, message: str, **kwargs) -> None: + """Log debug message""" + self.logger.debug(message, **kwargs) + self.log_counts['debug'] += 1 + + def info(self, message: str, **kwargs) -> None: + """Log info message""" + self.logger.info(message, **kwargs) + self.log_counts['info'] += 1 + + def warning(self, message: str, **kwargs) -> None: + """Log warning message""" + self.logger.warning(message, **kwargs) + self.log_counts['warning'] += 1 + + def error(self, message: str, **kwargs) -> None: + """Log error message""" + self.logger.error(message, **kwargs) + self.log_counts['error'] += 1 + + def critical(self, message: str, **kwargs) -> None: + """Log critical message""" + self.logger.critical(message, **kwargs) + self.log_counts['critical'] += 1 + + def success(self, message: str, **kwargs) -> None: + """Log success message (info level with special formatting)""" + # Use a custom success formatter for console + if self.logger.handlers: + console_handler = self.logger.handlers[0] + if hasattr(console_handler, 'formatter'): + original_format = console_handler.formatter.format + + def success_format(record): + return f"{Colors.GREEN}[✓] {record.getMessage()}{Colors.RESET}" + + console_handler.formatter.format = success_format + self.logger.info(message, **kwargs) + console_handler.formatter.format = original_format + else: + self.logger.info(f"SUCCESS: {message}", **kwargs) + else: + self.logger.info(f"SUCCESS: {message}", **kwargs) + + self.log_counts['info'] += 1 + + def step(self, step: int, total: int, message: str, **kwargs) -> None: + """Log step progress""" + step_msg = f"[{step}/{total}] {message}" + self.info(step_msg, **kwargs) + + def section(self, title: str, **kwargs) -> None: + """Log section header""" + separator = "=" * min(50, len(title) + 4) + self.info(separator, **kwargs) + self.info(f" {title}", **kwargs) + self.info(separator, **kwargs) + + def exception(self, message: str, exc_info: bool = True, **kwargs) -> None: + """Log exception with traceback""" + self.logger.error(message, exc_info=exc_info, **kwargs) + self.log_counts['error'] += 1 + + def log_system_info(self, info: Dict[str, Any]) -> None: + """Log system information""" + self.section("System Information") + for key, value in info.items(): + self.info(f"{key}: {value}") + + def log_operation_start(self, operation: str, details: Optional[Dict[str, Any]] = None) -> None: + """Log start of operation""" + self.section(f"Starting: {operation}") + if details: + for key, value in details.items(): + self.info(f"{key}: {value}") + + def log_operation_end(self, operation: str, success: bool, duration: float, details: Optional[Dict[str, Any]] = None) -> None: + """Log end of operation""" + status = "SUCCESS" if success else "FAILED" + self.info(f"Operation {operation} completed: {status} (Duration: {duration:.2f}s)") + + if details: + for key, value in details.items(): + self.info(f"{key}: {value}") + + def get_statistics(self) -> Dict[str, Any]: + """Get logging statistics""" + runtime = datetime.now() - self.session_start + + return { + 'session_start': self.session_start.isoformat(), + 'runtime_seconds': runtime.total_seconds(), + 'log_counts': self.log_counts.copy(), + 'total_messages': sum(self.log_counts.values()), + 'log_file': str(self.log_file) if hasattr(self, 'log_file') and self.log_file else None, + 'has_errors': self.log_counts['error'] + self.log_counts['critical'] > 0 + } + + def set_console_level(self, level: LogLevel) -> None: + """Change console logging level""" + self.console_level = level + if self.logger.handlers: + self.logger.handlers[0].setLevel(level.value) + + def set_file_level(self, level: LogLevel) -> None: + """Change file logging level""" + self.file_level = level + if len(self.logger.handlers) > 1: + self.logger.handlers[1].setLevel(level.value) + + def flush(self) -> None: + """Flush all handlers""" + for handler in self.logger.handlers: + if hasattr(handler, 'flush'): + handler.flush() + + def close(self) -> None: + """Close logger and handlers""" + self.section("Installation Session Complete") + stats = self.get_statistics() + + self.info(f"Total runtime: {stats['runtime_seconds']:.1f} seconds") + self.info(f"Messages logged: {stats['total_messages']}") + if stats['has_errors']: + self.warning(f"Errors/warnings: {stats['log_counts']['error'] + stats['log_counts']['warning']}") + + if stats['log_file']: + self.info(f"Full log saved to: {stats['log_file']}") + + # Close all handlers + for handler in self.logger.handlers[:]: + handler.close() + self.logger.removeHandler(handler) + + +# Global logger instance +_global_logger: Optional[Logger] = None + + +def get_logger(name: str = "superclaude") -> Logger: + """Get or create global logger instance""" + global _global_logger + + if _global_logger is None or _global_logger.name != name: + _global_logger = Logger(name) + + return _global_logger + + +def setup_logging(name: str = "superclaude", log_dir: Optional[Path] = None, console_level: LogLevel = LogLevel.INFO, file_level: LogLevel = LogLevel.DEBUG) -> Logger: + """Setup logging with specified configuration""" + global _global_logger + _global_logger = Logger(name, log_dir, console_level, file_level) + return _global_logger + + +# Convenience functions using global logger +def debug(message: str, **kwargs) -> None: + """Log debug message using global logger""" + get_logger().debug(message, **kwargs) + + +def info(message: str, **kwargs) -> None: + """Log info message using global logger""" + get_logger().info(message, **kwargs) + + +def warning(message: str, **kwargs) -> None: + """Log warning message using global logger""" + get_logger().warning(message, **kwargs) + + +def error(message: str, **kwargs) -> None: + """Log error message using global logger""" + get_logger().error(message, **kwargs) + + +def critical(message: str, **kwargs) -> None: + """Log critical message using global logger""" + get_logger().critical(message, **kwargs) + + +def success(message: str, **kwargs) -> None: + """Log success message using global logger""" + get_logger().success(message, **kwargs) \ No newline at end of file diff --git a/setup/utils/security.py b/setup/utils/security.py new file mode 100644 index 0000000..1a06d42 --- /dev/null +++ b/setup/utils/security.py @@ -0,0 +1,788 @@ +""" +Security utilities for SuperClaude installation system +Path validation and input sanitization + +This module provides comprehensive security validation for file paths and user inputs +during SuperClaude installation. It includes protection against: +- Directory traversal attacks +- Installation to system directories +- Path injection attacks +- Cross-platform security issues + +Key Features: +- Platform-specific validation (Windows vs Unix) +- User-friendly error messages with actionable suggestions +- Comprehensive path normalization +- Backward compatibility with existing validation logic + +Fixed Issues: +- GitHub Issue #129: Fixed overly broad regex patterns that prevented installation + in legitimate paths containing "dev", "tmp", "bin", etc. +- Enhanced cross-platform compatibility +- Improved error message clarity + +Architecture: +- Separated pattern categories for better maintainability +- Platform-aware validation logic +- Comprehensive test coverage +""" + +import re +import os +from pathlib import Path +from typing import List, Optional, Tuple, Set +import urllib.parse + + +class SecurityValidator: + """Security validation utilities""" + + # Directory traversal patterns (match anywhere in path - platform independent) + # These patterns detect common directory traversal attack vectors + TRAVERSAL_PATTERNS = [ + r'\.\./', # Directory traversal using ../ + r'\.\.\.', # Directory traversal using ... + r'//+', # Multiple consecutive slashes (path injection) + ] + + # Unix system directories (match only at start of path) + # These patterns identify Unix/Linux system directories that should not be writable + # by regular users. Using ^ anchor to match only at path start prevents false positives + # for user directories containing these names (e.g., /home/user/dev/ is allowed) + UNIX_SYSTEM_PATTERNS = [ + r'^/etc/', # System configuration files + r'^/bin/', # Essential command binaries + r'^/sbin/', # System binaries + r'^/usr/bin/', # User command binaries + r'^/usr/sbin/', # Non-essential system binaries + r'^/var/', # Variable data files + r'^/tmp/', # Temporary files (system-wide) + r'^/dev/', # Device files - FIXED: was r'/dev/' (GitHub Issue #129) + r'^/proc/', # Process information pseudo-filesystem + r'^/sys/', # System information pseudo-filesystem + ] + + # Windows system directories (match only at start of path) + # These patterns identify Windows system directories using flexible separator matching + # to handle both forward slashes and backslashes consistently + WINDOWS_SYSTEM_PATTERNS = [ + r'^c:[/\\]windows[/\\]', # Windows system directory + r'^c:[/\\]program files[/\\]', # Program Files directory + # Note: Removed c:\\users\\ to allow installation in user directories + # Claude Code installs to user home directory by default + ] + + # Combined dangerous patterns for backward compatibility + # This maintains compatibility with existing code while providing the new categorized approach + DANGEROUS_PATTERNS = TRAVERSAL_PATTERNS + UNIX_SYSTEM_PATTERNS + WINDOWS_SYSTEM_PATTERNS + + # Dangerous filename patterns + DANGEROUS_FILENAMES = [ + r'\.exe$', # Executables + r'\.bat$', + r'\.cmd$', + r'\.scr$', + r'\.dll$', + r'\.so$', + r'\.dylib$', + r'passwd', # System files + r'shadow', + r'hosts', + r'\.ssh/', + r'\.aws/', + r'\.env', # Environment files + r'\.secret', + ] + + # Allowed file extensions for installation + ALLOWED_EXTENSIONS = { + '.md', '.json', '.py', '.js', '.ts', '.jsx', '.tsx', + '.txt', '.yml', '.yaml', '.toml', '.cfg', '.conf', + '.sh', '.ps1', '.html', '.css', '.svg', '.png', '.jpg', '.gif' + } + + # Maximum path lengths + MAX_PATH_LENGTH = 4096 + MAX_FILENAME_LENGTH = 255 + + @classmethod + def validate_path(cls, path: Path, base_dir: Optional[Path] = None) -> Tuple[bool, str]: + """ + Validate path for security issues with enhanced cross-platform support + + This method performs comprehensive security validation including: + - Directory traversal attack detection + - System directory protection (platform-specific) + - Path length and filename validation + - Cross-platform path normalization + - User-friendly error messages + + Architecture: + - Uses both original and resolved paths for validation + - Applies platform-specific patterns for system directories + - Checks traversal patterns against original path to catch attacks before normalization + - Provides detailed error messages with actionable suggestions + + Args: + path: Path to validate (can be relative or absolute) + base_dir: Base directory that path should be within (optional) + + Returns: + Tuple of (is_safe: bool, error_message: str) + - is_safe: True if path passes all security checks + - error_message: Detailed error message with suggestions if validation fails + """ + try: + # Convert to absolute path + abs_path = path.resolve() + + # For system directory validation, use the original path structure + # to avoid issues with symlinks and cross-platform path resolution + original_path_str = cls._normalize_path_for_validation(path) + resolved_path_str = cls._normalize_path_for_validation(abs_path) + + # Check path length + if len(str(abs_path)) > cls.MAX_PATH_LENGTH: + return False, f"Path too long: {len(str(abs_path))} > {cls.MAX_PATH_LENGTH}" + + # Check filename length + if len(abs_path.name) > cls.MAX_FILENAME_LENGTH: + return False, f"Filename too long: {len(abs_path.name)} > {cls.MAX_FILENAME_LENGTH}" + + # Check for dangerous patterns using platform-specific validation + # Always check traversal patterns (platform independent) - use original path string + # to detect patterns before normalization removes them + original_str = str(path).lower() + for pattern in cls.TRAVERSAL_PATTERNS: + if re.search(pattern, original_str, re.IGNORECASE): + return False, cls._get_user_friendly_error_message("traversal", pattern, abs_path) + + # Check platform-specific system directory patterns - use original path first, then resolved + # Always check both Windows and Unix patterns to handle cross-platform scenarios + + # Check Windows system directory patterns + for pattern in cls.WINDOWS_SYSTEM_PATTERNS: + if (re.search(pattern, original_path_str, re.IGNORECASE) or + re.search(pattern, resolved_path_str, re.IGNORECASE)): + return False, cls._get_user_friendly_error_message("windows_system", pattern, abs_path) + + # Check Unix system directory patterns + for pattern in cls.UNIX_SYSTEM_PATTERNS: + if (re.search(pattern, original_path_str, re.IGNORECASE) or + re.search(pattern, resolved_path_str, re.IGNORECASE)): + return False, cls._get_user_friendly_error_message("unix_system", pattern, abs_path) + + # Check for dangerous filenames + for pattern in cls.DANGEROUS_FILENAMES: + if re.search(pattern, abs_path.name, re.IGNORECASE): + return False, f"Dangerous filename pattern detected: {pattern}" + + # Check if path is within base directory + if base_dir: + base_abs = base_dir.resolve() + try: + abs_path.relative_to(base_abs) + except ValueError: + return False, f"Path outside allowed directory: {abs_path} not in {base_abs}" + + # Check for null bytes + if '\x00' in str(path): + return False, "Null byte detected in path" + + # Check for Windows reserved names + if os.name == 'nt': + reserved_names = [ + 'CON', 'PRN', 'AUX', 'NUL', + 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', + 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' + ] + + name_without_ext = abs_path.stem.upper() + if name_without_ext in reserved_names: + return False, f"Reserved Windows filename: {name_without_ext}" + + return True, "Path is safe" + + except Exception as e: + return False, f"Path validation error: {e}" + + @classmethod + def validate_file_extension(cls, path: Path) -> Tuple[bool, str]: + """ + Validate file extension is allowed + + Args: + path: Path to validate + + Returns: + Tuple of (is_allowed: bool, message: str) + """ + extension = path.suffix.lower() + + if not extension: + return True, "No extension (allowed)" + + if extension in cls.ALLOWED_EXTENSIONS: + return True, f"Extension {extension} is allowed" + else: + return False, f"Extension {extension} is not allowed" + + @classmethod + def sanitize_filename(cls, filename: str) -> str: + """ + Sanitize filename by removing dangerous characters + + Args: + filename: Original filename + + Returns: + Sanitized filename + """ + # Remove null bytes + filename = filename.replace('\x00', '') + + # Remove or replace dangerous characters + dangerous_chars = r'[<>:"/\\|?*\x00-\x1f]' + filename = re.sub(dangerous_chars, '_', filename) + + # Remove leading/trailing dots and spaces + filename = filename.strip('. ') + + # Ensure not empty + if not filename: + filename = 'unnamed' + + # Truncate if too long + if len(filename) > cls.MAX_FILENAME_LENGTH: + name, ext = os.path.splitext(filename) + max_name_len = cls.MAX_FILENAME_LENGTH - len(ext) + filename = name[:max_name_len] + ext + + # Check for Windows reserved names + if os.name == 'nt': + name_without_ext = os.path.splitext(filename)[0].upper() + reserved_names = [ + 'CON', 'PRN', 'AUX', 'NUL', + 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', + 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' + ] + + if name_without_ext in reserved_names: + filename = f"safe_{filename}" + + return filename + + @classmethod + def sanitize_input(cls, user_input: str, max_length: int = 1000) -> str: + """ + Sanitize user input + + Args: + user_input: Raw user input + max_length: Maximum allowed length + + Returns: + Sanitized input + """ + if not user_input: + return "" + + # Remove null bytes and control characters + sanitized = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', user_input) + + # Trim whitespace + sanitized = sanitized.strip() + + # Truncate if too long + if len(sanitized) > max_length: + sanitized = sanitized[:max_length] + + return sanitized + + @classmethod + def validate_url(cls, url: str) -> Tuple[bool, str]: + """ + Validate URL for security issues + + Args: + url: URL to validate + + Returns: + Tuple of (is_safe: bool, message: str) + """ + try: + parsed = urllib.parse.urlparse(url) + + # Check scheme + if parsed.scheme not in ['http', 'https']: + return False, f"Invalid scheme: {parsed.scheme}" + + # Check for localhost/private IPs (basic check) + hostname = parsed.hostname + if hostname: + if hostname.lower() in ['localhost', '127.0.0.1', '::1']: + return False, "Localhost URLs not allowed" + + # Basic private IP check + if hostname.startswith('192.168.') or hostname.startswith('10.') or hostname.startswith('172.'): + return False, "Private IP addresses not allowed" + + # Check URL length + if len(url) > 2048: + return False, "URL too long" + + return True, "URL is safe" + + except Exception as e: + return False, f"URL validation error: {e}" + + @classmethod + def check_permissions(cls, path: Path, required_permissions: Set[str]) -> Tuple[bool, List[str]]: + """ + Check file/directory permissions + + Args: + path: Path to check + required_permissions: Set of required permissions ('read', 'write', 'execute') + + Returns: + Tuple of (has_permissions: bool, missing_permissions: List[str]) + """ + missing = [] + + try: + if not path.exists(): + # For non-existent paths, check parent directory + parent = path.parent + if not parent.exists(): + missing.append("path does not exist") + return False, missing + path = parent + + if 'read' in required_permissions: + if not os.access(path, os.R_OK): + missing.append('read') + + if 'write' in required_permissions: + if not os.access(path, os.W_OK): + missing.append('write') + + if 'execute' in required_permissions: + if not os.access(path, os.X_OK): + missing.append('execute') + + return len(missing) == 0, missing + + except Exception as e: + missing.append(f"permission check error: {e}") + return False, missing + + @classmethod + def validate_installation_target(cls, target_dir: Path) -> Tuple[bool, List[str]]: + """ + Validate installation target directory with enhanced Windows compatibility + + Args: + target_dir: Target installation directory + + Returns: + Tuple of (is_safe: bool, error_messages: List[str]) + """ + errors = [] + + # Enhanced path resolution with Windows normalization + try: + abs_target = target_dir.resolve() + except Exception as e: + errors.append(f"Cannot resolve target path: {e}") + return False, errors + + # Windows-specific path normalization + if os.name == 'nt': + # Normalize Windows paths for consistent comparison + abs_target_str = str(abs_target).lower().replace('/', '\\') + else: + abs_target_str = str(abs_target).lower() + + # Special handling for Claude installation directory + claude_patterns = ['.claude', '.claude' + os.sep, '.claude\\', '.claude/'] + is_claude_dir = any(abs_target_str.endswith(pattern) for pattern in claude_patterns) + + if is_claude_dir: + try: + home_path = Path.home() + except (RuntimeError, OSError): + # If we can't determine home directory, skip .claude special handling + cls._log_security_decision("WARN", f"Cannot determine home directory for .claude validation: {abs_target}") + # Fall through to regular validation + else: + try: + # Verify it's specifically the current user's home directory + abs_target.relative_to(home_path) + + # Enhanced Windows security checks for .claude directories + if os.name == 'nt': + # Check for junction points and symbolic links on Windows + if cls._is_windows_junction_or_symlink(abs_target): + errors.append("Installation to junction points or symbolic links is not allowed for security") + return False, errors + + # Additional validation: verify it's in the current user's profile directory + # Use actual home directory comparison instead of username-based path construction + if ':' in abs_target_str and '\\users\\' in abs_target_str: + try: + # Check if target is within the user's actual home directory + home_path = Path.home() + abs_target.relative_to(home_path) + # Path is valid - within user's home directory + except ValueError: + # Path is outside user's home directory + current_user = os.environ.get('USERNAME', home_path.name) + errors.append(f"Installation must be in current user's directory ({current_user})") + return False, errors + + # Check permissions + has_perms, missing = cls.check_permissions(target_dir, {'read', 'write'}) + if not has_perms: + if os.name == 'nt': + errors.append(f"Insufficient permissions for Windows installation: {missing}. Try running as administrator or check folder permissions.") + else: + errors.append(f"Insufficient permissions: missing {missing}") + + # Log successful validation for audit trail + cls._log_security_decision("ALLOW", f"Claude directory installation validated: {abs_target}") + return len(errors) == 0, errors + + except ValueError: + # Not under current user's home directory + if os.name == 'nt': + errors.append("Claude installation must be in your user directory (e.g., C:\\Users\\YourName\\.claude)") + else: + errors.append("Claude installation must be in your home directory (e.g., ~/.claude)") + cls._log_security_decision("DENY", f"Claude directory outside user home: {abs_target}") + return False, errors + + # Validate path for non-.claude directories + is_safe, msg = cls.validate_path(target_dir) + if not is_safe: + if os.name == 'nt': + # Enhanced Windows error messages + if "dangerous path pattern" in msg.lower(): + errors.append(f"Invalid Windows path: {msg}. Ensure path doesn't contain dangerous patterns or reserved directories.") + elif "path too long" in msg.lower(): + errors.append(f"Windows path too long: {msg}. Windows has a 260 character limit for most paths.") + elif "reserved" in msg.lower(): + errors.append(f"Windows reserved name: {msg}. Avoid names like CON, PRN, AUX, NUL, COM1-9, LPT1-9.") + else: + errors.append(f"Invalid target path: {msg}") + else: + errors.append(f"Invalid target path: {msg}") + + # Check permissions with platform-specific guidance + has_perms, missing = cls.check_permissions(target_dir, {'read', 'write'}) + if not has_perms: + if os.name == 'nt': + errors.append(f"Insufficient Windows permissions: {missing}. Try running as administrator or check folder security settings in Properties > Security.") + else: + errors.append(f"Insufficient permissions: {missing}. Try: chmod 755 {target_dir}") + + # Check if it's a system directory with enhanced messages + system_dirs = [ + Path('/etc'), Path('/bin'), Path('/sbin'), Path('/usr/bin'), Path('/usr/sbin'), + Path('/var'), Path('/tmp'), Path('/dev'), Path('/proc'), Path('/sys') + ] + + if os.name == 'nt': + system_dirs.extend([ + Path('C:\\Windows'), Path('C:\\Program Files'), Path('C:\\Program Files (x86)') + ]) + + for sys_dir in system_dirs: + try: + if abs_target.is_relative_to(sys_dir): + if os.name == 'nt': + errors.append(f"Cannot install to Windows system directory: {sys_dir}. Use a location in your user profile instead (e.g., C:\\Users\\YourName\\).") + else: + errors.append(f"Cannot install to system directory: {sys_dir}. Use a location in your home directory instead (~/).") + cls._log_security_decision("DENY", f"Attempted installation to system directory: {sys_dir}") + break + except (ValueError, AttributeError): + # is_relative_to not available in older Python versions + try: + abs_target.relative_to(sys_dir) + errors.append(f"Cannot install to system directory: {sys_dir}") + break + except ValueError: + continue + + return len(errors) == 0, errors + + @classmethod + def validate_component_files(cls, file_list: List[Tuple[Path, Path]], base_source_dir: Path, base_target_dir: Path) -> Tuple[bool, List[str]]: + """ + Validate list of files for component installation + + Args: + file_list: List of (source, target) path tuples + base_source_dir: Base source directory + base_target_dir: Base target directory + + Returns: + Tuple of (all_safe: bool, error_messages: List[str]) + """ + errors = [] + + for source, target in file_list: + # Validate source path + is_safe, msg = cls.validate_path(source, base_source_dir) + if not is_safe: + errors.append(f"Invalid source path {source}: {msg}") + + # Validate target path + is_safe, msg = cls.validate_path(target, base_target_dir) + if not is_safe: + errors.append(f"Invalid target path {target}: {msg}") + + # Validate file extension + is_allowed, msg = cls.validate_file_extension(source) + if not is_allowed: + errors.append(f"File {source}: {msg}") + + return len(errors) == 0, errors + + @classmethod + def _normalize_path_for_validation(cls, path: Path) -> str: + """ + Normalize path for consistent validation across platforms + + Args: + path: Path to normalize + + Returns: + Normalized path string for validation + """ + path_str = str(path) + + # Convert to lowercase for case-insensitive comparison + path_str = path_str.lower() + + # Normalize path separators for consistent pattern matching + if os.name == 'nt': # Windows + # Convert forward slashes to backslashes for Windows + path_str = path_str.replace('/', '\\') + # Ensure consistent drive letter format + if len(path_str) >= 2 and path_str[1] == ':': + path_str = path_str[0] + ':\\' + path_str[3:].lstrip('\\') + else: # Unix-like systems + # Convert backslashes to forward slashes for Unix + path_str = path_str.replace('\\', '/') + # Ensure single leading slash + if path_str.startswith('//'): + path_str = '/' + path_str.lstrip('/') + + return path_str + + @classmethod + def _get_user_friendly_error_message(cls, error_type: str, pattern: str, path: Path) -> str: + """ + Generate user-friendly error messages with actionable suggestions + + Args: + error_type: Type of error (traversal, windows_system, unix_system) + pattern: The regex pattern that matched + path: The path that caused the error + + Returns: + User-friendly error message with suggestions + """ + if error_type == "traversal": + return ( + f"Security violation: Directory traversal pattern detected in path '{path}'. " + f"Paths containing '..' or '//' are not allowed for security reasons. " + f"Please use an absolute path without directory traversal characters." + ) + elif error_type == "windows_system": + if pattern == r'^c:\\windows\\': + return ( + f"Cannot install to Windows system directory '{path}'. " + f"Please choose a location in your user directory instead, " + f"such as C:\\Users\\{os.environ.get('USERNAME', 'YourName')}\\.claude\\" + ) + elif pattern == r'^c:\\program files\\': + return ( + f"Cannot install to Program Files directory '{path}'. " + f"Please choose a location in your user directory instead, " + f"such as C:\\Users\\{os.environ.get('USERNAME', 'YourName')}\\.claude\\" + ) + else: + return ( + f"Cannot install to Windows system directory '{path}'. " + f"Please choose a location in your user directory instead." + ) + elif error_type == "unix_system": + system_dirs = { + r'^/dev/': "/dev (device files)", + r'^/etc/': "/etc (system configuration)", + r'^/bin/': "/bin (system binaries)", + r'^/sbin/': "/sbin (system binaries)", + r'^/usr/bin/': "/usr/bin (user binaries)", + r'^/usr/sbin/': "/usr/sbin (user system binaries)", + r'^/var/': "/var (variable data)", + r'^/tmp/': "/tmp (temporary files)", + r'^/proc/': "/proc (process information)", + r'^/sys/': "/sys (system information)" + } + + dir_desc = system_dirs.get(pattern, "system directory") + return ( + f"Cannot install to {dir_desc} '{path}'. " + f"Please choose a location in your home directory instead, " + f"such as ~/.claude/ or ~/SuperClaude/" + ) + else: + return f"Security validation failed for path '{path}'" + + @classmethod + def _is_windows_junction_or_symlink(cls, path: Path) -> bool: + """ + Check if path is a Windows junction point or symbolic link + + Args: + path: Path to check + + Returns: + True if path is a junction point or symlink, False otherwise + """ + if os.name != 'nt': + return False + + try: + # Only check if path exists to avoid filesystem errors during testing + if not path.exists(): + return False + + # Check if path is a symlink (covers most cases) + if path.is_symlink(): + return True + + # Additional Windows-specific checks for junction points + try: + import stat + st = path.stat() + # Check for reparse point (junction points have this attribute) + if hasattr(st, 'st_reparse_tag') and st.st_reparse_tag != 0: + return True + except (OSError, AttributeError): + pass + + # Alternative method using os.path.islink + try: + if os.path.islink(str(path)): + return True + except (OSError, AttributeError): + pass + + except (OSError, AttributeError, NotImplementedError): + # If we can't determine safely, default to False + # This ensures the function doesn't break validation + pass + + return False + + @classmethod + def _log_security_decision(cls, action: str, message: str) -> None: + """ + Log security validation decisions for audit trail + + Args: + action: Security action taken (ALLOW, DENY, WARN) + message: Description of the decision + """ + try: + import logging + import datetime + + # Create security logger if it doesn't exist + security_logger = logging.getLogger('superclaude.security') + if not security_logger.handlers: + # Set up basic logging if not already configured + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - SECURITY - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + security_logger.addHandler(handler) + security_logger.setLevel(logging.INFO) + + # Log the security decision + timestamp = datetime.datetime.now().isoformat() + log_message = f"[{action}] {message} (PID: {os.getpid()})" + + if action == "DENY": + security_logger.warning(log_message) + else: + security_logger.info(log_message) + + except Exception: + # Don't fail security validation if logging fails + pass + + @classmethod + def create_secure_temp_dir(cls, prefix: str = "superclaude_") -> Path: + """ + Create secure temporary directory + + Args: + prefix: Prefix for temp directory name + + Returns: + Path to secure temporary directory + """ + import tempfile + + # Create with secure permissions (0o700) + temp_dir = Path(tempfile.mkdtemp(prefix=prefix)) + temp_dir.chmod(0o700) + + return temp_dir + + @classmethod + def secure_delete(cls, path: Path) -> bool: + """ + Securely delete file or directory + + Args: + path: Path to delete + + Returns: + True if successful, False otherwise + """ + try: + if not path.exists(): + return True + + if path.is_file(): + # Overwrite file with random data before deletion + try: + import secrets + file_size = path.stat().st_size + + with open(path, 'r+b') as f: + # Overwrite with random data + f.write(secrets.token_bytes(file_size)) + f.flush() + os.fsync(f.fileno()) + except Exception: + pass # If overwrite fails, still try to delete + + path.unlink() + + elif path.is_dir(): + # Recursively delete directory contents + import shutil + shutil.rmtree(path) + + return True + + except Exception: + return False \ No newline at end of file diff --git a/setup/utils/ui.py b/setup/utils/ui.py new file mode 100644 index 0000000..7356cba --- /dev/null +++ b/setup/utils/ui.py @@ -0,0 +1,434 @@ +""" +User interface utilities for SuperClaude installation system +Cross-platform console UI with colors and progress indication +""" + +import sys +import time +import shutil +from typing import List, Optional, Any, Dict, Union +from enum import Enum + +# Try to import colorama for cross-platform color support +try: + import colorama + from colorama import Fore, Back, Style + colorama.init(autoreset=True) + COLORAMA_AVAILABLE = True +except ImportError: + COLORAMA_AVAILABLE = False + # Fallback color codes for Unix-like systems + class MockFore: + RED = '\033[91m' if sys.platform != 'win32' else '' + GREEN = '\033[92m' if sys.platform != 'win32' else '' + YELLOW = '\033[93m' if sys.platform != 'win32' else '' + BLUE = '\033[94m' if sys.platform != 'win32' else '' + MAGENTA = '\033[95m' if sys.platform != 'win32' else '' + CYAN = '\033[96m' if sys.platform != 'win32' else '' + WHITE = '\033[97m' if sys.platform != 'win32' else '' + + class MockStyle: + RESET_ALL = '\033[0m' if sys.platform != 'win32' else '' + BRIGHT = '\033[1m' if sys.platform != 'win32' else '' + + Fore = MockFore() + Style = MockStyle() + + +class Colors: + """Color constants for console output""" + RED = Fore.RED + GREEN = Fore.GREEN + YELLOW = Fore.YELLOW + BLUE = Fore.BLUE + MAGENTA = Fore.MAGENTA + CYAN = Fore.CYAN + WHITE = Fore.WHITE + RESET = Style.RESET_ALL + BRIGHT = Style.BRIGHT + + +class ProgressBar: + """Cross-platform progress bar with customizable display""" + + def __init__(self, total: int, width: int = 50, prefix: str = '', suffix: str = ''): + """ + Initialize progress bar + + Args: + total: Total number of items to process + width: Width of progress bar in characters + prefix: Text to display before progress bar + suffix: Text to display after progress bar + """ + self.total = total + self.width = width + self.prefix = prefix + self.suffix = suffix + self.current = 0 + self.start_time = time.time() + + # Get terminal width for responsive display + try: + self.terminal_width = shutil.get_terminal_size().columns + except OSError: + self.terminal_width = 80 + + def update(self, current: int, message: str = '') -> None: + """ + Update progress bar + + Args: + current: Current progress value + message: Optional message to display + """ + self.current = current + percent = min(100, (current / self.total) * 100) if self.total > 0 else 100 + + # Calculate filled and empty portions + filled_width = int(self.width * current / self.total) if self.total > 0 else self.width + filled = '█' * filled_width + empty = '░' * (self.width - filled_width) + + # Calculate elapsed time and ETA + elapsed = time.time() - self.start_time + if current > 0: + eta = (elapsed / current) * (self.total - current) + eta_str = f" ETA: {self._format_time(eta)}" + else: + eta_str = "" + + # Format progress line + if message: + status = f" {message}" + else: + status = "" + + progress_line = ( + f"\r{self.prefix}[{Colors.GREEN}{filled}{Colors.WHITE}{empty}{Colors.RESET}] " + f"{percent:5.1f}%{status}{eta_str}" + ) + + # Truncate if too long for terminal + max_length = self.terminal_width - 5 + if len(progress_line) > max_length: + # Remove color codes for length calculation + plain_line = progress_line.replace(Colors.GREEN, '').replace(Colors.WHITE, '').replace(Colors.RESET, '') + if len(plain_line) > max_length: + progress_line = progress_line[:max_length] + "..." + + print(progress_line, end='', flush=True) + + def increment(self, message: str = '') -> None: + """ + Increment progress by 1 + + Args: + message: Optional message to display + """ + self.update(self.current + 1, message) + + def finish(self, message: str = 'Complete') -> None: + """ + Complete progress bar + + Args: + message: Completion message + """ + self.update(self.total, message) + print() # New line after completion + + def _format_time(self, seconds: float) -> str: + """Format time duration as human-readable string""" + if seconds < 60: + return f"{seconds:.0f}s" + elif seconds < 3600: + return f"{seconds/60:.0f}m {seconds%60:.0f}s" + else: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return f"{hours:.0f}h {minutes:.0f}m" + + +class Menu: + """Interactive menu system with keyboard navigation""" + + def __init__(self, title: str, options: List[str], multi_select: bool = False): + """ + Initialize menu + + Args: + title: Menu title + options: List of menu options + multi_select: Allow multiple selections + """ + self.title = title + self.options = options + self.multi_select = multi_select + self.selected = set() if multi_select else None + + def display(self) -> Union[int, List[int]]: + """ + Display menu and get user selection + + Returns: + Selected option index (single) or list of indices (multi-select) + """ + print(f"\n{Colors.CYAN}{Colors.BRIGHT}{self.title}{Colors.RESET}") + print("=" * len(self.title)) + + for i, option in enumerate(self.options, 1): + if self.multi_select: + marker = "[x]" if i-1 in (self.selected or set()) else "[ ]" + print(f"{Colors.YELLOW}{i:2d}.{Colors.RESET} {marker} {option}") + else: + print(f"{Colors.YELLOW}{i:2d}.{Colors.RESET} {option}") + + if self.multi_select: + print(f"\n{Colors.BLUE}Enter numbers separated by commas (e.g., 1,3,5) or 'all' for all options:{Colors.RESET}") + else: + print(f"\n{Colors.BLUE}Enter your choice (1-{len(self.options)}):{Colors.RESET}") + + while True: + try: + user_input = input("> ").strip().lower() + + if self.multi_select: + if user_input == 'all': + return list(range(len(self.options))) + elif user_input == '': + return [] + else: + # Parse comma-separated numbers + selections = [] + for part in user_input.split(','): + part = part.strip() + if part.isdigit(): + idx = int(part) - 1 + if 0 <= idx < len(self.options): + selections.append(idx) + else: + raise ValueError(f"Invalid option: {part}") + else: + raise ValueError(f"Invalid input: {part}") + return list(set(selections)) # Remove duplicates + else: + if user_input.isdigit(): + choice = int(user_input) - 1 + if 0 <= choice < len(self.options): + return choice + else: + print(f"{Colors.RED}Invalid choice. Please enter a number between 1 and {len(self.options)}.{Colors.RESET}") + else: + print(f"{Colors.RED}Please enter a valid number.{Colors.RESET}") + + except (ValueError, KeyboardInterrupt) as e: + if isinstance(e, KeyboardInterrupt): + print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}") + return [] if self.multi_select else -1 + else: + print(f"{Colors.RED}Invalid input: {e}{Colors.RESET}") + + +def confirm(message: str, default: bool = True) -> bool: + """ + Ask for user confirmation + + Args: + message: Confirmation message + default: Default response if user just presses Enter + + Returns: + True if confirmed, False otherwise + """ + suffix = "[Y/n]" if default else "[y/N]" + print(f"{Colors.BLUE}{message} {suffix}{Colors.RESET}") + + while True: + try: + response = input("> ").strip().lower() + + if response == '': + return default + elif response in ['y', 'yes', 'true', '1']: + return True + elif response in ['n', 'no', 'false', '0']: + return False + else: + print(f"{Colors.RED}Please enter 'y' or 'n' (or press Enter for default).{Colors.RESET}") + + except KeyboardInterrupt: + print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}") + return False + + +def display_header(title: str, subtitle: str = '') -> None: + """ + Display formatted header + + Args: + title: Main title + subtitle: Optional subtitle + """ + print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}{title:^60}{Colors.RESET}") + if subtitle: + print(f"{Colors.WHITE}{subtitle:^60}{Colors.RESET}") + print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}\n") + + +def display_info(message: str) -> None: + """Display info message""" + print(f"{Colors.BLUE}[INFO] {message}{Colors.RESET}") + + +def display_success(message: str) -> None: + """Display success message""" + print(f"{Colors.GREEN}[✓] {message}{Colors.RESET}") + + +def display_warning(message: str) -> None: + """Display warning message""" + print(f"{Colors.YELLOW}[!] {message}{Colors.RESET}") + + +def display_error(message: str) -> None: + """Display error message""" + print(f"{Colors.RED}[✗] {message}{Colors.RESET}") + + +def display_step(step: int, total: int, message: str) -> None: + """Display step progress""" + print(f"{Colors.CYAN}[{step}/{total}] {message}{Colors.RESET}") + + +def display_table(headers: List[str], rows: List[List[str]], title: str = '') -> None: + """ + Display data in table format + + Args: + headers: Column headers + rows: Data rows + title: Optional table title + """ + if not rows: + return + + # Calculate column widths + col_widths = [len(header) for header in headers] + for row in rows: + for i, cell in enumerate(row): + if i < len(col_widths): + col_widths[i] = max(col_widths[i], len(str(cell))) + + # Display title + if title: + print(f"\n{Colors.CYAN}{Colors.BRIGHT}{title}{Colors.RESET}") + print() + + # Display headers + header_line = " | ".join(f"{header:<{col_widths[i]}}" for i, header in enumerate(headers)) + print(f"{Colors.YELLOW}{header_line}{Colors.RESET}") + print("-" * len(header_line)) + + # Display rows + for row in rows: + row_line = " | ".join(f"{str(cell):<{col_widths[i]}}" for i, cell in enumerate(row)) + print(row_line) + + print() + + +def wait_for_key(message: str = "Press Enter to continue...") -> None: + """Wait for user to press a key""" + try: + input(f"{Colors.BLUE}{message}{Colors.RESET}") + except KeyboardInterrupt: + print(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}") + + +def clear_screen() -> None: + """Clear terminal screen""" + import os + os.system('cls' if os.name == 'nt' else 'clear') + + +class StatusSpinner: + """Simple status spinner for long operations""" + + def __init__(self, message: str = "Working..."): + """ + Initialize spinner + + Args: + message: Message to display with spinner + """ + self.message = message + self.spinning = False + self.chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" + self.current = 0 + + def start(self) -> None: + """Start spinner in background thread""" + import threading + + def spin(): + while self.spinning: + char = self.chars[self.current % len(self.chars)] + print(f"\r{Colors.BLUE}{char} {self.message}{Colors.RESET}", end='', flush=True) + self.current += 1 + time.sleep(0.1) + + self.spinning = True + self.thread = threading.Thread(target=spin, daemon=True) + self.thread.start() + + def stop(self, final_message: str = '') -> None: + """ + Stop spinner + + Args: + final_message: Final message to display + """ + self.spinning = False + if hasattr(self, 'thread'): + self.thread.join(timeout=0.2) + + # Clear spinner line + print(f"\r{' ' * (len(self.message) + 5)}\r", end='') + + if final_message: + print(final_message) + + +def format_size(size_bytes: int) -> str: + """Format file size in human-readable format""" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if size_bytes < 1024.0: + return f"{size_bytes:.1f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.1f} PB" + + +def format_duration(seconds: float) -> str: + """Format duration in human-readable format""" + if seconds < 1: + return f"{seconds*1000:.0f}ms" + elif seconds < 60: + return f"{seconds:.1f}s" + elif seconds < 3600: + minutes = seconds // 60 + secs = seconds % 60 + return f"{minutes:.0f}m {secs:.0f}s" + else: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return f"{hours:.0f}h {minutes:.0f}m" + + +def truncate_text(text: str, max_length: int, suffix: str = "...") -> str: + """Truncate text to maximum length with optional suffix""" + if len(text) <= max_length: + return text + + return text[:max_length - len(suffix)] + suffix diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 47513f2..0000000 --- a/uv.lock +++ /dev/null @@ -1,42 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.8" -resolution-markers = [ - "python_full_version >= '3.9'", - "python_full_version < '3.9'", -] - -[[package]] -name = "setuptools" -version = "75.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198 }, -] - -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, -] - -[[package]] -name = "superclaude" -source = { editable = "." } -dependencies = [ - { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] - -[package.metadata] -requires-dist = [{ name = "setuptools", specifier = ">=45.0.0" }]