444 lines
14 KiB
Python
444 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""CLI script for /align-project-retrofit command.
|
|
|
|
This script orchestrates the brownfield retrofit process:
|
|
1. Analyze codebase structure and tech stack
|
|
2. Assess alignment with autonomous-dev standards
|
|
3. Generate migration plan
|
|
4. Execute migration (with backup/rollback)
|
|
5. Verify results and assess readiness
|
|
|
|
Usage:
|
|
python align_project_retrofit.py [options]
|
|
|
|
Exit Codes:
|
|
0: Success
|
|
1: Error
|
|
2: Verification failed (blockers present)
|
|
|
|
Related:
|
|
- GitHub Issue #59: Brownfield retrofit command implementation
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from lib.codebase_analyzer import CodebaseAnalyzer
|
|
from lib.alignment_assessor import AlignmentAssessor
|
|
from lib.migration_planner import MigrationPlanner
|
|
from lib.retrofit_executor import RetrofitExecutor, ExecutionMode
|
|
from lib.retrofit_verifier import RetrofitVerifier
|
|
from lib.security_utils import audit_log
|
|
|
|
|
|
def parse_args():
|
|
"""Parse command-line arguments.
|
|
|
|
Returns:
|
|
Parsed arguments
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Retrofit brownfield projects for autonomous development",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Full retrofit (step-by-step)
|
|
python align_project_retrofit.py
|
|
|
|
# Analyze only
|
|
python align_project_retrofit.py --phase analyze
|
|
|
|
# Plan only
|
|
python align_project_retrofit.py --phase plan
|
|
|
|
# Dry-run execution
|
|
python align_project_retrofit.py --dry-run
|
|
|
|
# Auto-execute all steps
|
|
python align_project_retrofit.py --auto
|
|
|
|
# JSON output
|
|
python align_project_retrofit.py --json
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--project-root",
|
|
type=str,
|
|
default=".",
|
|
help="Project root directory (default: current directory)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--phase",
|
|
type=str,
|
|
choices=["analyze", "assess", "plan", "execute", "verify", "all"],
|
|
default="all",
|
|
help="Which phase to run (default: all)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would happen without making changes"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--auto",
|
|
action="store_true",
|
|
help="Execute all steps automatically (no confirmations)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Output JSON for scripting"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
action="store_true",
|
|
help="Verbose output"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output",
|
|
type=str,
|
|
help="Output file for results (default: stdout)"
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def run_analyze_phase(project_root: Path, verbose: bool = False) -> dict:
|
|
"""Run analysis phase.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
verbose: Enable verbose output
|
|
|
|
Returns:
|
|
Analysis results as dict
|
|
"""
|
|
if verbose:
|
|
print("PHASE 1: Analyzing codebase...")
|
|
|
|
analyzer = CodebaseAnalyzer(project_root)
|
|
analysis = analyzer.analyze()
|
|
|
|
if verbose:
|
|
print(f" Tech Stack: {analysis.tech_stack.primary_language}")
|
|
print(f" Framework: {analysis.tech_stack.framework or 'None'}")
|
|
print(f" Files: {analysis.structure.total_files}")
|
|
print(f" Tests: {analysis.structure.test_files}")
|
|
|
|
return {
|
|
"phase": "analyze",
|
|
"tech_stack": {
|
|
"primary_language": analysis.tech_stack.primary_language,
|
|
"framework": analysis.tech_stack.framework,
|
|
"package_manager": analysis.tech_stack.package_manager,
|
|
"test_framework": analysis.tech_stack.test_framework,
|
|
"dependencies": list(analysis.tech_stack.dependencies)[:20] # Top 20
|
|
},
|
|
"structure": {
|
|
"total_files": analysis.structure.total_files,
|
|
"source_files": analysis.structure.source_files,
|
|
"test_files": analysis.structure.test_files,
|
|
"config_files": analysis.structure.config_files,
|
|
"doc_files": analysis.structure.doc_files,
|
|
"has_src_dir": analysis.structure.has_src_dir,
|
|
"has_tests_dir": analysis.structure.has_tests_dir,
|
|
"has_docs_dir": analysis.structure.has_docs_dir
|
|
},
|
|
"analysis_object": analysis # For next phase
|
|
}
|
|
|
|
|
|
def run_assess_phase(project_root: Path, analysis_result: dict, verbose: bool = False) -> dict:
|
|
"""Run assessment phase.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
analysis_result: Results from analyze phase
|
|
verbose: Enable verbose output
|
|
|
|
Returns:
|
|
Assessment results as dict
|
|
"""
|
|
if verbose:
|
|
print("PHASE 2: Assessing alignment...")
|
|
|
|
assessor = AlignmentAssessor(project_root)
|
|
assessment = assessor.assess(analysis_result["analysis_object"])
|
|
|
|
if verbose:
|
|
print(f" 12-Factor Score: {assessment.twelve_factor_score.compliance_percentage:.1f}%")
|
|
print(f" Alignment Gaps: {len(assessment.gaps)}")
|
|
print(f" PROJECT.md Confidence: {assessment.project_md.confidence:.2f}")
|
|
|
|
return {
|
|
"phase": "assess",
|
|
"twelve_factor_score": assessment.twelve_factor_score.compliance_percentage,
|
|
"gap_count": len(assessment.gaps),
|
|
"priority_gaps": [
|
|
{
|
|
"category": gap.category,
|
|
"severity": gap.severity.value,
|
|
"description": gap.description,
|
|
"impact": gap.impact_score,
|
|
"effort": gap.effort_hours
|
|
}
|
|
for gap in assessment.priority_list[:5] # Top 5
|
|
],
|
|
"project_md_confidence": assessment.project_md.confidence,
|
|
"assessment_object": assessment # For next phase
|
|
}
|
|
|
|
|
|
def run_plan_phase(project_root: Path, assessment_result: dict, verbose: bool = False) -> dict:
|
|
"""Run planning phase.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
assessment_result: Results from assess phase
|
|
verbose: Enable verbose output
|
|
|
|
Returns:
|
|
Planning results as dict
|
|
"""
|
|
if verbose:
|
|
print("PHASE 3: Generating migration plan...")
|
|
|
|
planner = MigrationPlanner(project_root)
|
|
plan = planner.plan(assessment_result["assessment_object"])
|
|
|
|
if verbose:
|
|
print(f" Migration Steps: {len(plan.steps)}")
|
|
print(f" Total Effort: {plan.total_effort_hours:.1f} hours")
|
|
print(f" Critical Path: {plan.critical_path_hours:.1f} hours")
|
|
|
|
return {
|
|
"phase": "plan",
|
|
"step_count": len(plan.steps),
|
|
"total_effort_hours": plan.total_effort_hours,
|
|
"critical_path_hours": plan.critical_path_hours,
|
|
"steps": [
|
|
{
|
|
"step_id": step.step_id,
|
|
"title": step.title,
|
|
"effort_size": step.effort_size.value,
|
|
"effort_hours": step.effort_hours,
|
|
"impact": step.impact_level.value
|
|
}
|
|
for step in plan.steps
|
|
],
|
|
"plan_object": plan # For next phase
|
|
}
|
|
|
|
|
|
def run_execute_phase(
|
|
project_root: Path,
|
|
plan_result: dict,
|
|
mode: ExecutionMode,
|
|
verbose: bool = False
|
|
) -> dict:
|
|
"""Run execution phase.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
plan_result: Results from plan phase
|
|
mode: Execution mode
|
|
verbose: Enable verbose output
|
|
|
|
Returns:
|
|
Execution results as dict
|
|
"""
|
|
if verbose:
|
|
mode_str = "DRY RUN" if mode == ExecutionMode.DRY_RUN else "EXECUTING"
|
|
print(f"PHASE 4: {mode_str} migration...")
|
|
|
|
executor = RetrofitExecutor(project_root)
|
|
execution = executor.execute(plan_result["plan_object"], mode)
|
|
|
|
if verbose:
|
|
print(f" Completed: {len(execution.completed_steps)}")
|
|
print(f" Failed: {len(execution.failed_steps)}")
|
|
if execution.backup:
|
|
print(f" Backup: {execution.backup.backup_path}")
|
|
if execution.rollback_performed:
|
|
print(" Rollback: PERFORMED")
|
|
|
|
return {
|
|
"phase": "execute",
|
|
"mode": mode.value,
|
|
"completed": len(execution.completed_steps),
|
|
"failed": len(execution.failed_steps),
|
|
"rollback": execution.rollback_performed,
|
|
"backup_path": str(execution.backup.backup_path) if execution.backup else None,
|
|
"execution_object": execution # For next phase
|
|
}
|
|
|
|
|
|
def run_verify_phase(project_root: Path, execution_result: dict, verbose: bool = False) -> dict:
|
|
"""Run verification phase.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
execution_result: Results from execute phase
|
|
verbose: Enable verbose output
|
|
|
|
Returns:
|
|
Verification results as dict
|
|
"""
|
|
if verbose:
|
|
print("PHASE 5: Verifying results...")
|
|
|
|
verifier = RetrofitVerifier(project_root)
|
|
verification = verifier.verify(execution_result["execution_object"])
|
|
|
|
if verbose:
|
|
print(f" Readiness Score: {verification.readiness_score:.1f}%")
|
|
print(f" Compliance Checks: {len([c for c in verification.compliance_checks if c.passed])}/{len(verification.compliance_checks)} passed")
|
|
print(f" Blockers: {len(verification.blockers)}")
|
|
print(f" Ready for /auto-implement: {'YES' if verification.ready_for_auto_implement else 'NO'}")
|
|
|
|
return {
|
|
"phase": "verify",
|
|
"readiness_score": verification.readiness_score,
|
|
"checks_passed": len([c for c in verification.compliance_checks if c.passed]),
|
|
"checks_total": len(verification.compliance_checks),
|
|
"blockers": verification.blockers,
|
|
"ready_for_auto_implement": verification.ready_for_auto_implement,
|
|
"verification_object": verification
|
|
}
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
args = parse_args()
|
|
|
|
# Resolve project root
|
|
project_root = Path(args.project_root).resolve()
|
|
|
|
audit_log(
|
|
"align_project_retrofit_start",
|
|
project_root=str(project_root),
|
|
phase=args.phase,
|
|
dry_run=args.dry_run,
|
|
auto=args.auto
|
|
)
|
|
|
|
try:
|
|
results = {}
|
|
|
|
# Determine execution mode
|
|
if args.dry_run:
|
|
exec_mode = ExecutionMode.DRY_RUN
|
|
elif args.auto:
|
|
exec_mode = ExecutionMode.AUTO
|
|
else:
|
|
exec_mode = ExecutionMode.STEP_BY_STEP
|
|
|
|
# Run requested phases
|
|
if args.phase in ["analyze", "all"]:
|
|
results["analyze"] = run_analyze_phase(project_root, args.verbose)
|
|
|
|
if args.phase in ["assess", "all"] and "analyze" in results:
|
|
results["assess"] = run_assess_phase(project_root, results["analyze"], args.verbose)
|
|
|
|
if args.phase in ["plan", "all"] and "assess" in results:
|
|
results["plan"] = run_plan_phase(project_root, results["assess"], args.verbose)
|
|
|
|
if args.phase in ["execute", "all"] and "plan" in results:
|
|
results["execute"] = run_execute_phase(
|
|
project_root,
|
|
results["plan"],
|
|
exec_mode,
|
|
args.verbose
|
|
)
|
|
|
|
if args.phase in ["verify", "all"] and "execute" in results:
|
|
results["verify"] = run_verify_phase(project_root, results["execute"], args.verbose)
|
|
|
|
# Clean up non-serializable objects
|
|
for phase_key in results:
|
|
if "analysis_object" in results[phase_key]:
|
|
del results[phase_key]["analysis_object"]
|
|
if "assessment_object" in results[phase_key]:
|
|
del results[phase_key]["assessment_object"]
|
|
if "plan_object" in results[phase_key]:
|
|
del results[phase_key]["plan_object"]
|
|
if "execution_object" in results[phase_key]:
|
|
del results[phase_key]["execution_object"]
|
|
if "verification_object" in results[phase_key]:
|
|
del results[phase_key]["verification_object"]
|
|
|
|
# Output results
|
|
if args.json:
|
|
output = json.dumps(results, indent=2)
|
|
if args.output:
|
|
Path(args.output).write_text(output)
|
|
else:
|
|
print(output)
|
|
else:
|
|
# Human-readable output
|
|
if not args.verbose:
|
|
print("\n=== Retrofit Complete ===\n")
|
|
if "verify" in results:
|
|
verify = results["verify"]
|
|
print(f"Readiness Score: {verify['readiness_score']:.1f}%")
|
|
print(f"Compliance: {verify['checks_passed']}/{verify['checks_total']} checks passed")
|
|
print(f"Blockers: {len(verify['blockers'])}")
|
|
print(f"Ready for /auto-implement: {'YES' if verify['ready_for_auto_implement'] else 'NO'}")
|
|
|
|
if verify['blockers']:
|
|
print("\nBlockers:")
|
|
for blocker in verify['blockers']:
|
|
print(f" - {blocker}")
|
|
|
|
audit_log(
|
|
"align_project_retrofit_complete",
|
|
project_root=str(project_root),
|
|
success=True
|
|
)
|
|
|
|
# Exit code based on verification results
|
|
if "verify" in results:
|
|
if results["verify"]["blockers"]:
|
|
return 2 # Blockers present
|
|
return 0 # Success
|
|
return 0
|
|
|
|
except Exception as e:
|
|
audit_log(
|
|
"align_project_retrofit_failed",
|
|
project_root=str(project_root),
|
|
error=str(e),
|
|
success=False
|
|
)
|
|
|
|
if args.json:
|
|
error_output = {
|
|
"error": str(e),
|
|
"success": False
|
|
}
|
|
if args.output:
|
|
Path(args.output).write_text(json.dumps(error_output, indent=2))
|
|
else:
|
|
print(json.dumps(error_output, indent=2))
|
|
else:
|
|
print(f"ERROR: {e}", file=sys.stderr)
|
|
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|