Files
dl-organizer/src/vlm/commands/execute.py
T

261 lines
9.7 KiB
Python
Raw Normal View History

"""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
2026-04-02 11:31:49 +08:00
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...")
2026-04-02 11:31:49 +08:00
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)}")
2026-04-02 11:31:49 +08:00
plan_summary = preferred_plan_summary(execution_plan)
if plan_summary:
click.echo()
2026-04-02 11:31:49 +08:00
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"],
)