"""Execute and rollback command implementations.""" from __future__ import annotations from pathlib import Path from typing import Optional import click from vlm.context import CLIContext from vlm.executor import ExecutionEngine from vlm.plan_render import preferred_plan_summary from vlm.planner import load_plan from vlm.state import StateManager def _validate_plan_structure(execution_plan) -> list[str]: """Validate source/destination paths for move/rename operations.""" validation_errors: list[str] = [] for operation in execution_plan.operations: if operation.operation_type in ("move", "rename"): if not operation.source_path.exists(): validation_errors.append(f"Source file does not exist: {operation.source_path}") elif not operation.source_path.is_file(): validation_errors.append(f"Source path is not a file: {operation.source_path}") for operation in execution_plan.operations: if operation.operation_type in ("move", "rename") and operation.destination_path: dest_dir = operation.destination_path.parent if not dest_dir.exists() and not dest_dir.parent.exists(): validation_errors.append(f"Cannot create destination directory (parent missing): {dest_dir}") return validation_errors def execute_cmd( ctx: CLIContext, plan: Path, confirm: bool, yes: bool, verbose_ops: bool, preserve_directories: bool, safe_mode: bool, require_review: bool = False, review_csv: Path | None = None, ) -> None: """Execute an execution plan in dry-run or execute mode.""" config = ctx.config logger = ctx.logger mode = "execute" if confirm else "dry-run" click.echo(f"Loading execution plan from: {plan}") click.echo() execution_plan = load_plan(plan) if require_review and confirm: from vlm.plan_review import check_review_requirements csv_path = review_csv or plan.parent / "plan_manual_review.csv" review_errors = check_review_requirements(execution_plan, plan, csv_path) if review_errors: formatted = "\n".join(f" - {e}" for e in review_errors) raise ValueError(f"REVIEW REQUIRED BEFORE EXECUTE:\n{formatted}") if preserve_directories: click.echo("Directory preservation is enabled (plan metadata/operations will be honored).") click.echo() if safe_mode: click.echo("Safe mode enabled - validating plan for directory preservation...") validation_snapshot = execution_plan.metadata.get("validation_snapshot", {}) if not isinstance(validation_snapshot, dict): validation_snapshot = {} emptied_dirs = validation_snapshot.get("emptied_directories", execution_plan.metadata.get("emptied_directories", [])) if emptied_dirs: raise ValueError( "SAFE MODE VIOLATION: plan would empty directories. " "Regenerate/adjust the plan to preserve directory structure." ) click.echo("Safe mode validation passed - no directories would be destroyed") click.echo() click.echo("Validating directory structure...") validation_errors = _validate_plan_structure(execution_plan) if validation_errors: formatted = "\n".join(f" - {error}" for error in validation_errors[:10]) if len(validation_errors) > 10: formatted += f"\n ... and {len(validation_errors) - 10} more errors" raise ValueError(f"DIRECTORY STRUCTURE VALIDATION FAILED:\n{formatted}") click.echo("Directory structure validation passed") click.echo() click.echo(f"Execution plan loaded: {execution_plan.plan_id}") click.echo(f"Created at: {execution_plan.created_at}") click.echo(f"Total operations: {len(execution_plan.operations)}") plan_summary = preferred_plan_summary(execution_plan) if plan_summary: click.echo() click.echo(plan_summary) click.echo() if mode == "dry-run": click.echo("DRY-RUN MODE - No files will be modified") click.echo("Use --confirm to actually execute operations") else: click.echo("EXECUTE MODE - Files will be modified") click.echo() if not yes and not click.confirm("Are you sure you want to proceed?"): click.echo("Execution cancelled.") return if yes: click.echo("Auto-approved via --yes flag") click.echo() click.echo(f"Executing {len(execution_plan.operations)} operations...") click.echo() state_path = Path.home() / ".vlm" / "state.json" state_manager = StateManager(state_path) engine = ExecutionEngine( logger=logger, config=config, verbose_operations=verbose_ops, state_manager=state_manager, ) results, summary, rollback_log = engine.execute_plan( execution_plan, mode=mode, confirmed=confirm, ) if mode == "dry-run": click.echo("Sample operations (dry-run):") for i, result in enumerate(results[:5]): op = result.operation if op.operation_type != "no-op": click.echo(f" [{i + 1}] {op.operation_type}: {op.source_path.name}") if op.destination_path: click.echo(f" -> {op.destination_path}") if len(results) > 5: click.echo(f" ... and {len(results) - 5} more operations") else: for i, result in enumerate(results): op = result.operation if op.operation_type != "no-op" and not op.has_conflict: status = "OK" if result.success else "FAIL" click.echo(f" [{i + 1}/{len(results)}] {status} {op.operation_type}: {op.source_path.name}") if result.error_message: click.echo(f" Error: {result.error_message}") click.echo() click.echo("=" * 60) click.echo(f"Execution Summary ({mode} mode)") click.echo("=" * 60) click.echo(f" Total operations: {summary['total']}") click.echo(f" Successful: {summary['successful']}") click.echo(f" Failed: {summary['failed']}") click.echo(f" Skipped: {summary['skipped']}") click.echo() if mode == "execute" and rollback_log: rollback_dir = Path.home() / ".vlm" / "rollback" rollback_dir.mkdir(parents=True, exist_ok=True) rollback_path = rollback_dir / f"rollback_{rollback_log.log_id}.json" engine.save_rollback_log(rollback_log, rollback_path) click.echo(f"Rollback log saved to: {rollback_path}") click.echo() click.echo("To undo these operations, run:") click.echo(f" vlm rollback --log {rollback_path}") click.echo() if mode == "dry-run": click.echo("Dry-run complete! No files were modified.") click.echo("Review the operations above and use --confirm to execute.") elif summary["failed"] > 0: click.echo(f"Execution completed with {summary['failed']} failures.") click.echo("Check the log file for details.") else: click.echo("Execution completed successfully!") logger.info( "Execution completed in %s mode: %s successful, %s failed, %s skipped", mode, summary["successful"], summary["failed"], summary["skipped"], ) def rollback_cmd(ctx: CLIContext, log: Optional[Path]) -> None: """Rollback a prior execution from rollback log.""" config = ctx.config logger = ctx.logger if log is None: rollback_dir = Path.home() / ".vlm" / "rollback" if not rollback_dir.exists(): raise FileNotFoundError(f"No rollback logs found. Rollback directory does not exist: {rollback_dir}") rollback_logs = sorted( rollback_dir.glob("rollback_*.json"), key=lambda p: p.stat().st_mtime, reverse=True ) if not rollback_logs: raise FileNotFoundError(f"No rollback logs found. No rollback_*.json files in: {rollback_dir}") log = rollback_logs[0] click.echo(f"Loading rollback log: {log}") engine = ExecutionEngine(logger=logger, config=config) rollback_log = engine.load_rollback_log(log) click.echo(f"Rollback log loaded: {rollback_log.log_id}") click.echo(f"Plan ID: {rollback_log.execution_plan_id}") click.echo(f"Executed at: {rollback_log.executed_at}") click.echo(f"Operations to rollback: {len(rollback_log.operations)}") click.echo() if not click.confirm("Proceed with rollback?"): click.echo("Rollback cancelled.") return click.echo(f"Rolling back {len(rollback_log.operations)} operations...") click.echo() click.echo("Performing rollback...") rollback_results, rollback_summary = engine.rollback(rollback_log) click.echo() click.echo("Rollback Summary") click.echo("=" * 60) click.echo(f" Total operations: {rollback_summary['total']}") click.echo(f" Successful: {rollback_summary['successful']}") click.echo(f" Failed: {rollback_summary['failed']}") click.echo(f" Skipped: {rollback_summary['skipped']}") click.echo() if rollback_summary["failed"] > 0: click.echo("Failed operations:") for result in rollback_results: if not result.success: op = result.operation click.echo(f" - {op.operation_type}: {op.source_path}") if result.error_message: click.echo(f" Error: {result.error_message}") click.echo() if rollback_summary["failed"] == 0: click.echo("Rollback completed successfully!") else: click.echo(f"Rollback completed with {rollback_summary['failed']} failures.") logger.info( "Rollback completed: %s successful, %s failed", rollback_summary["successful"], rollback_summary["failed"], )