375 lines
13 KiB
Python
375 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Installation Analyzer - Analyze installation type and recommend strategy
|
|
|
|
This module analyzes project state to determine installation type (fresh,
|
|
brownfield, upgrade) and recommends an appropriate installation strategy.
|
|
|
|
Key Features:
|
|
- Installation type detection (fresh/brownfield/upgrade)
|
|
- Conflict report generation
|
|
- Risk assessment (low/medium/high)
|
|
- Strategy recommendation with action items
|
|
- Comprehensive analysis reports
|
|
|
|
Usage:
|
|
from installation_analyzer import InstallationAnalyzer, InstallationType
|
|
|
|
# Analyze project
|
|
analyzer = InstallationAnalyzer(project_dir)
|
|
install_type = analyzer.detect_installation_type()
|
|
strategy = analyzer.recommend_strategy()
|
|
|
|
Date: 2025-12-09
|
|
Issue: #106 (GenAI-first installation system)
|
|
Agent: implementer
|
|
|
|
Design Patterns:
|
|
See library-design-patterns skill for standardized design patterns.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
from datetime import datetime
|
|
|
|
# Import staging manager for conflict detection
|
|
try:
|
|
from plugins.autonomous_dev.lib.staging_manager import StagingManager
|
|
from plugins.autonomous_dev.lib.protected_file_detector import ProtectedFileDetector
|
|
from plugins.autonomous_dev.lib.security_utils import audit_log
|
|
except ImportError:
|
|
from staging_manager import StagingManager
|
|
from protected_file_detector import ProtectedFileDetector
|
|
from security_utils import audit_log
|
|
|
|
|
|
class InstallationType(Enum):
|
|
"""Installation type enumeration."""
|
|
FRESH = "fresh"
|
|
BROWNFIELD = "brownfield"
|
|
UPGRADE = "upgrade"
|
|
|
|
|
|
class InstallationAnalyzer:
|
|
"""Analyze installation type and recommend strategy.
|
|
|
|
This class analyzes project state to determine installation type and
|
|
recommend an appropriate installation strategy.
|
|
|
|
Attributes:
|
|
project_dir: Path to project directory
|
|
|
|
Examples:
|
|
>>> analyzer = InstallationAnalyzer(project_dir)
|
|
>>> install_type = analyzer.detect_installation_type()
|
|
>>> print(f"Installation type: {install_type.value}")
|
|
"""
|
|
|
|
def __init__(self, project_dir: Path | str):
|
|
"""Initialize installation analyzer.
|
|
|
|
Args:
|
|
project_dir: Path to project directory
|
|
|
|
Raises:
|
|
ValueError: If project directory doesn't exist
|
|
"""
|
|
project_path = Path(project_dir) if isinstance(project_dir, str) else project_dir
|
|
project_path = project_path.resolve()
|
|
|
|
if not project_path.exists():
|
|
raise ValueError(f"Project directory does not exist: {project_path}")
|
|
|
|
self.project_dir = project_path
|
|
|
|
# Audit log initialization
|
|
audit_log("installation_analyzer", "initialized", {
|
|
"project_dir": str(self.project_dir)
|
|
})
|
|
|
|
def detect_installation_type(self) -> InstallationType:
|
|
"""Detect installation type based on project state.
|
|
|
|
Returns:
|
|
InstallationType enum (FRESH, BROWNFIELD, or UPGRADE)
|
|
|
|
Detection Logic:
|
|
- FRESH: No .claude/ directory
|
|
- BROWNFIELD: Has PROJECT.md or user artifacts, but no plugin files
|
|
- UPGRADE: Has existing plugin files (commands, hooks, agents)
|
|
|
|
Examples:
|
|
>>> analyzer = InstallationAnalyzer(project_dir)
|
|
>>> install_type = analyzer.detect_installation_type()
|
|
"""
|
|
claude_dir = self.project_dir / ".claude"
|
|
|
|
# FRESH: No .claude directory
|
|
if not claude_dir.exists():
|
|
return InstallationType.FRESH
|
|
|
|
# Check for plugin files
|
|
has_commands = (claude_dir / "commands").exists()
|
|
has_hooks = (claude_dir / "hooks").exists()
|
|
has_agents = (claude_dir / "agents").exists()
|
|
|
|
# UPGRADE: Has plugin infrastructure
|
|
if has_commands or has_hooks or has_agents:
|
|
return InstallationType.UPGRADE
|
|
|
|
# Check for user artifacts
|
|
has_project_md = (claude_dir / "PROJECT.md").exists()
|
|
has_env = (self.project_dir / ".env").exists()
|
|
has_state = (claude_dir / "batch_state.json").exists()
|
|
|
|
# BROWNFIELD: Has user artifacts but no plugin files
|
|
if has_project_md or has_env or has_state:
|
|
return InstallationType.BROWNFIELD
|
|
|
|
# Default to FRESH if .claude exists but is empty
|
|
return InstallationType.FRESH
|
|
|
|
def generate_conflict_report(self, staging_dir: Path | str) -> Dict[str, Any]:
|
|
"""Generate conflict report between staging and project.
|
|
|
|
Args:
|
|
staging_dir: Path to staging directory
|
|
|
|
Returns:
|
|
Dict with conflict report:
|
|
- total_conflicts: Number of conflicts
|
|
- conflicts: List of conflict dicts
|
|
- conflict_categories: Dict of category counts
|
|
- total_staging_files: Number of files in staging
|
|
|
|
Raises:
|
|
ValueError: If staging directory doesn't exist
|
|
|
|
Examples:
|
|
>>> report = analyzer.generate_conflict_report(staging_dir)
|
|
>>> print(f"Found {report['total_conflicts']} conflicts")
|
|
"""
|
|
staging_path = Path(staging_dir) if isinstance(staging_dir, str) else staging_dir
|
|
staging_path = staging_path.resolve()
|
|
|
|
if not staging_path.exists():
|
|
raise ValueError(f"Staging directory not found: {staging_path}")
|
|
|
|
# Use StagingManager to detect conflicts
|
|
manager = StagingManager(staging_path)
|
|
conflicts = manager.detect_conflicts(self.project_dir)
|
|
|
|
# Use ProtectedFileDetector to categorize conflicts
|
|
detector = ProtectedFileDetector()
|
|
|
|
# Categorize each conflict
|
|
categorized_conflicts = []
|
|
category_counts = {}
|
|
|
|
for conflict in conflicts:
|
|
file_path = conflict["file"]
|
|
full_path = self.project_dir / file_path
|
|
|
|
# Determine category
|
|
category = "modified_plugin"
|
|
if detector.matches_pattern(file_path):
|
|
if file_path.endswith("PROJECT.md") or ".env" in file_path:
|
|
category = "config"
|
|
elif "custom_" in file_path:
|
|
category = "custom_hook"
|
|
|
|
categorized_conflicts.append({
|
|
**conflict,
|
|
"category": category
|
|
})
|
|
|
|
# Count categories
|
|
category_counts[category] = category_counts.get(category, 0) + 1
|
|
|
|
# Get total staging files
|
|
staging_files = manager.list_files()
|
|
|
|
return {
|
|
"total_conflicts": len(categorized_conflicts),
|
|
"conflicts": categorized_conflicts,
|
|
"conflict_categories": category_counts,
|
|
"total_staging_files": len(staging_files)
|
|
}
|
|
|
|
def recommend_strategy(self) -> Dict[str, Any]:
|
|
"""Recommend installation strategy based on project state.
|
|
|
|
Returns:
|
|
Dict with strategy recommendation:
|
|
- approach: Strategy name (copy_all, skip_protected, backup_and_merge)
|
|
- risk: Risk level (low, medium, high)
|
|
- action_items: List of recommended actions
|
|
- protected_files: List of protected files (if applicable)
|
|
- conflicts: Conflict info (if applicable)
|
|
- manual_review_recommended: True if high risk
|
|
|
|
Examples:
|
|
>>> strategy = analyzer.recommend_strategy()
|
|
>>> print(f"Recommended approach: {strategy['approach']}")
|
|
"""
|
|
install_type = self.detect_installation_type()
|
|
|
|
# FRESH: Simple copy all
|
|
if install_type == InstallationType.FRESH:
|
|
return {
|
|
"approach": "copy_all",
|
|
"risk": "low",
|
|
"action_items": [
|
|
"Copy all plugin files to .claude/",
|
|
"No user artifacts to protect"
|
|
]
|
|
}
|
|
|
|
# BROWNFIELD: Skip protected files
|
|
if install_type == InstallationType.BROWNFIELD:
|
|
detector = ProtectedFileDetector()
|
|
protected = detector.detect_protected_files(self.project_dir)
|
|
|
|
return {
|
|
"approach": "skip_protected",
|
|
"risk": "low",
|
|
"action_items": [
|
|
"Copy plugin files to .claude/",
|
|
f"Skip {len(protected)} protected user files"
|
|
],
|
|
"protected_files": [f["path"] for f in protected]
|
|
}
|
|
|
|
# UPGRADE: Backup and merge
|
|
if install_type == InstallationType.UPGRADE:
|
|
detector = ProtectedFileDetector()
|
|
protected = detector.detect_protected_files(self.project_dir)
|
|
|
|
# Assess risk based on number of modifications
|
|
risk_level = "low"
|
|
if len(protected) > 5:
|
|
risk_level = "medium"
|
|
if len(protected) > 15:
|
|
risk_level = "high"
|
|
|
|
strategy = {
|
|
"approach": "backup_and_merge",
|
|
"risk": risk_level,
|
|
"action_items": [
|
|
"Create backups of conflicting files",
|
|
"Copy new plugin files",
|
|
f"Preserve {len(protected)} protected files"
|
|
],
|
|
"conflicts": len(protected)
|
|
}
|
|
|
|
# Add manual review recommendation for high risk
|
|
if risk_level == "high":
|
|
strategy["manual_review_recommended"] = True
|
|
strategy["action_items"].append("MANUAL REVIEW RECOMMENDED: Many user modifications detected")
|
|
|
|
return strategy
|
|
|
|
# Fallback (should not reach here)
|
|
return {
|
|
"approach": "manual",
|
|
"risk": "high",
|
|
"action_items": ["Manual installation recommended"],
|
|
"manual_review_recommended": True
|
|
}
|
|
|
|
def assess_risk(self) -> Dict[str, Any]:
|
|
"""Assess installation risk.
|
|
|
|
Returns:
|
|
Dict with risk assessment:
|
|
- level: Risk level (low, medium, high)
|
|
- data_loss_risk: Boolean indicating data loss risk
|
|
- factors: List of contributing factors
|
|
- protected_files_count: Number of protected files
|
|
- conflicts_count: Estimated conflicts
|
|
|
|
Examples:
|
|
>>> risk = analyzer.assess_risk()
|
|
>>> print(f"Risk level: {risk['level']}")
|
|
"""
|
|
install_type = self.detect_installation_type()
|
|
|
|
# FRESH installation: Low risk
|
|
if install_type == InstallationType.FRESH:
|
|
return {
|
|
"level": "low",
|
|
"data_loss_risk": False,
|
|
"factors": ["No existing files to overwrite"],
|
|
"protected_files_count": 0,
|
|
"conflicts_count": 0
|
|
}
|
|
|
|
# Detect protected files
|
|
detector = ProtectedFileDetector()
|
|
protected = detector.detect_protected_files(self.project_dir)
|
|
|
|
# BROWNFIELD: Low to medium risk
|
|
if install_type == InstallationType.BROWNFIELD:
|
|
return {
|
|
"level": "low" if len(protected) < 5 else "medium",
|
|
"data_loss_risk": False, # Protected files preserved
|
|
"factors": [
|
|
f"{len(protected)} user artifacts will be protected",
|
|
"No plugin files to conflict with"
|
|
],
|
|
"protected_files_count": len(protected),
|
|
"conflicts_count": 0
|
|
}
|
|
|
|
# UPGRADE: Medium to high risk
|
|
risk_level = "low"
|
|
factors = []
|
|
|
|
if len(protected) > 5:
|
|
risk_level = "medium"
|
|
factors.append("Multiple user modifications detected")
|
|
|
|
if len(protected) > 15:
|
|
risk_level = "high"
|
|
factors.append("Extensive customizations present")
|
|
|
|
return {
|
|
"level": risk_level,
|
|
"data_loss_risk": False, # Backup strategy prevents data loss
|
|
"factors": factors or ["Some user modifications present"],
|
|
"protected_files_count": len(protected),
|
|
"conflicts_count": len(protected)
|
|
}
|
|
|
|
def generate_analysis_report(self, staging_dir: Path | str) -> Dict[str, Any]:
|
|
"""Generate comprehensive analysis report.
|
|
|
|
Args:
|
|
staging_dir: Path to staging directory
|
|
|
|
Returns:
|
|
Dict with complete analysis:
|
|
- timestamp: ISO 8601 timestamp
|
|
- project_dir: Project directory path
|
|
- staging_dir: Staging directory path
|
|
- installation_type: Detected installation type
|
|
- conflicts: Conflict report
|
|
- strategy: Recommended strategy
|
|
- risk: Risk assessment
|
|
|
|
Examples:
|
|
>>> report = analyzer.generate_analysis_report(staging_dir)
|
|
>>> print(report["installation_type"])
|
|
"""
|
|
return {
|
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
"project_dir": str(self.project_dir),
|
|
"staging_dir": str(staging_dir),
|
|
"installation_type": self.detect_installation_type().value,
|
|
"conflicts": self.generate_conflict_report(staging_dir),
|
|
"strategy": self.recommend_strategy(),
|
|
"risk": self.assess_risk()
|
|
}
|