SuperClaude/setup/managers/claude_md_manager.py

316 lines
12 KiB
Python
Raw Normal View History

"""
CLAUDE.md Manager for preserving user customizations while managing framework imports
"""
import re
from pathlib import Path
from typing import List, Set, Dict, Optional
from ..utils.logger import get_logger
class CLAUDEMdManager:
"""Manages CLAUDE.md file updates while preserving user customizations"""
def __init__(self, install_dir: Path):
"""
Initialize CLAUDEMdManager
Args:
install_dir: Installation directory (typically ~/.claude)
"""
self.install_dir = install_dir
self.claude_md_path = install_dir / "CLAUDE.md"
self.logger = get_logger()
def read_existing_imports(self) -> Set[str]:
"""
Parse CLAUDE.md for existing @import statements
Returns:
Set of already imported filenames (without @)
"""
existing_imports = set()
if not self.claude_md_path.exists():
return existing_imports
try:
with open(self.claude_md_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find all @import statements using regex
import_pattern = r'^@([^\s\n]+\.md)\s*$'
matches = re.findall(import_pattern, content, re.MULTILINE)
existing_imports.update(matches)
self.logger.debug(f"Found existing imports: {existing_imports}")
except Exception as e:
self.logger.warning(f"Could not read existing CLAUDE.md imports: {e}")
return existing_imports
def read_existing_content(self) -> str:
"""
Read existing CLAUDE.md content
Returns:
Existing content or empty string if file doesn't exist
"""
if not self.claude_md_path.exists():
return ""
try:
with open(self.claude_md_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
self.logger.warning(f"Could not read existing CLAUDE.md: {e}")
return ""
def extract_user_content(self, content: str) -> str:
"""
Extract user content (everything before framework imports section)
Args:
content: Full CLAUDE.md content
Returns:
User content without framework imports
"""
# Look for framework imports section marker
framework_marker = "# ═══════════════════════════════════════════════════\n# SuperClaude Framework Components"
if framework_marker in content:
user_content = content.split(framework_marker)[0].rstrip()
else:
# If no framework section exists, preserve all content
user_content = content.rstrip()
return user_content
def organize_imports_by_category(self, files_by_category: Dict[str, List[str]]) -> str:
"""
Organize imports into categorized sections
Args:
files_by_category: Dict mapping category names to lists of files
Returns:
Formatted import sections
"""
if not files_by_category:
return ""
sections = []
# Framework imports section header
sections.append("# ═══════════════════════════════════════════════════")
sections.append("# SuperClaude Framework Components")
sections.append("# ═══════════════════════════════════════════════════")
sections.append("")
# Add each category
for category, files in files_by_category.items():
if files:
sections.append(f"# {category}")
for file in sorted(files):
sections.append(f"@{file}")
sections.append("")
return "\n".join(sections)
def add_imports(self, files: List[str], category: str = "Framework") -> bool:
"""
Add new imports with duplicate checking and user content preservation
Args:
files: List of filenames to import
category: Category name for organizing imports
Returns:
True if successful, False otherwise
"""
try:
# Ensure CLAUDE.md exists
self.ensure_claude_md_exists()
# Read existing content and imports
existing_content = self.read_existing_content()
existing_imports = self.read_existing_imports()
# Filter out files already imported
new_files = [f for f in files if f not in existing_imports]
if not new_files:
self.logger.info("All files already imported, no changes needed")
return True
self.logger.info(f"Adding {len(new_files)} new imports to category '{category}': {new_files}")
# Extract user content (preserve everything before framework section)
user_content = self.extract_user_content(existing_content)
# Parse existing framework imports by category
existing_framework_imports = self._parse_existing_framework_imports(existing_content)
# Add new files to the specified category
if category not in existing_framework_imports:
existing_framework_imports[category] = []
existing_framework_imports[category].extend(new_files)
# Build new content
new_content_parts = []
# Add user content
if user_content.strip():
new_content_parts.append(user_content)
new_content_parts.append("") # Add blank line before framework section
# Add organized framework imports
framework_section = self.organize_imports_by_category(existing_framework_imports)
if framework_section:
new_content_parts.append(framework_section)
# Write updated content
new_content = "\n".join(new_content_parts)
with open(self.claude_md_path, 'w', encoding='utf-8') as f:
f.write(new_content)
self.logger.success(f"Updated CLAUDE.md with {len(new_files)} new imports")
return True
except Exception as e:
self.logger.error(f"Failed to update CLAUDE.md: {e}")
return False
def _parse_existing_framework_imports(self, content: str) -> Dict[str, List[str]]:
"""
Parse existing framework imports organized by category
Args:
content: Full CLAUDE.md content
Returns:
Dict mapping category names to lists of imported files
"""
imports_by_category = {}
# Look for framework imports section
framework_marker = "# ═══════════════════════════════════════════════════\n# SuperClaude Framework Components"
if framework_marker not in content:
return imports_by_category
# Extract framework section
framework_section = content.split(framework_marker)[1] if framework_marker in content else ""
# Parse categories and imports
lines = framework_section.split('\n')
current_category = None
for line in lines:
line = line.strip()
# Skip section header lines and empty lines
if line.startswith('# ═══') or not line:
continue
# Category header (starts with # but not the section divider)
if line.startswith('# ') and not line.startswith('# ═══'):
current_category = line[2:].strip() # Remove "# "
if current_category not in imports_by_category:
imports_by_category[current_category] = []
# Import line (starts with @)
elif line.startswith('@') and current_category:
import_file = line[1:].strip() # Remove "@"
if import_file not in imports_by_category[current_category]:
imports_by_category[current_category].append(import_file)
return imports_by_category
def ensure_claude_md_exists(self) -> None:
"""
Create CLAUDE.md with default content if it doesn't exist
"""
if self.claude_md_path.exists():
return
try:
# Create directory if it doesn't exist
self.claude_md_path.parent.mkdir(parents=True, exist_ok=True)
# Default CLAUDE.md content
default_content = """# SuperClaude Entry Point
This file serves as the entry point for the SuperClaude framework.
You can add your own custom instructions and configurations here.
The SuperClaude framework components will be automatically imported below.
"""
with open(self.claude_md_path, 'w', encoding='utf-8') as f:
f.write(default_content)
self.logger.info("Created CLAUDE.md with default content")
except Exception as e:
self.logger.error(f"Failed to create CLAUDE.md: {e}")
raise
def remove_imports(self, files: List[str]) -> bool:
"""
Remove specific imports from CLAUDE.md
Args:
files: List of filenames to remove from imports
Returns:
True if successful, False otherwise
"""
try:
if not self.claude_md_path.exists():
return True # Nothing to remove
existing_content = self.read_existing_content()
user_content = self.extract_user_content(existing_content)
existing_framework_imports = self._parse_existing_framework_imports(existing_content)
# Remove files from all categories
removed_any = False
for category, category_files in existing_framework_imports.items():
for file in files:
if file in category_files:
category_files.remove(file)
removed_any = True
# Remove empty categories
existing_framework_imports = {k: v for k, v in existing_framework_imports.items() if v}
if not removed_any:
return True # Nothing was removed
# Rebuild content
new_content_parts = []
if user_content.strip():
new_content_parts.append(user_content)
new_content_parts.append("")
framework_section = self.organize_imports_by_category(existing_framework_imports)
if framework_section:
new_content_parts.append(framework_section)
# Write updated content
new_content = "\n".join(new_content_parts)
with open(self.claude_md_path, 'w', encoding='utf-8') as f:
f.write(new_content)
self.logger.info(f"Removed {len(files)} imports from CLAUDE.md")
return True
except Exception as e:
self.logger.error(f"Failed to remove imports from CLAUDE.md: {e}")
return False