"""Action dataclass and ActionExecutor for plan-then-execute workflows.""" from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional from flow.core.console import ConsoleLogger @dataclass class Action: type: str description: str data: Dict[str, Any] = field(default_factory=dict) skip_on_error: bool = True os_filter: Optional[str] = None status: str = "pending" error: Optional[str] = None class ActionExecutor: """Register handlers for action types, then execute a plan.""" def __init__(self, console: ConsoleLogger): self.console = console self._handlers: Dict[str, Callable] = {} self.post_comments: List[str] = [] def register(self, action_type: str, handler: Callable) -> None: self._handlers[action_type] = handler def execute(self, actions: List[Action], *, dry_run: bool = False, current_os: str = "") -> None: if dry_run: self._print_plan(actions) return # Filter OS-incompatible actions compatible = [a for a in actions if a.os_filter is None or a.os_filter == current_os] skipped_count = len(actions) - len(compatible) if skipped_count: self.console.info(f"Skipped {skipped_count} OS-incompatible actions") self.console.section_header(f"EXECUTING {len(compatible)} ACTIONS") for i, action in enumerate(compatible, 1): self.console.step_start(i, len(compatible), action.description) handler = self._handlers.get(action.type) if not handler: action.status = "skipped" self.console.step_skip(f"No handler for action type: {action.type}") continue try: handler(action.data) action.status = "completed" self.console.step_complete() except Exception as e: action.error = str(e) if action.skip_on_error: action.status = "skipped" self.console.step_skip(str(e)) else: action.status = "failed" self.console.step_fail(str(e)) print(f"\n{self.console.RED}Critical action failed, stopping execution{self.console.RESET}") break self._print_summary(compatible) def _print_plan(self, actions: List[Action]) -> None: self.console.plan_header("EXECUTION PLAN", len(actions)) grouped: Dict[str, List[Action]] = {} for action in actions: category = action.type.split("-")[0] grouped.setdefault(category, []).append(action) for category, category_actions in grouped.items(): self.console.plan_category(category) for i, action in enumerate(category_actions, 1): self.console.plan_item( i, action.description, action.os_filter, not action.skip_on_error, ) self.console.plan_legend() def _print_summary(self, actions: List[Action]) -> None: completed = sum(1 for a in actions if a.status == "completed") failed = sum(1 for a in actions if a.status == "failed") skipped = sum(1 for a in actions if a.status == "skipped") self.console.section_summary("EXECUTION SUMMARY") c = self.console print(f"Total actions: {c.BOLD}{len(actions)}{c.RESET}") print(f"Completed: {c.GREEN}{completed}{c.RESET}") if failed: print(f"Failed: {c.RED}{failed}{c.RESET}") if skipped: print(f"Skipped: {c.YELLOW}{skipped}{c.RESET}") if self.post_comments: print(f"\n{c.BOLD}POST-INSTALL NOTES{c.RESET}") print(f"{c.CYAN}{'-' * 25}{c.RESET}") for i, comment in enumerate(self.post_comments, 1): print(f"{i}. {comment}") if failed: print(f"\n{c.BOLD}FAILED ACTIONS{c.RESET}") print(f"{c.RED}{'-' * 20}{c.RESET}") for action in actions: if action.status == "failed": print(f"{c.RED}>{c.RESET} {action.description}") print(f" {c.GRAY}Error: {action.error}{c.RESET}") print(f"\n{c.RED}{failed} action(s) failed. Check the errors above.{c.RESET}") else: print(f"\n{c.GREEN}All actions completed successfully!{c.RESET}")