crackerjack 0.30.3__py3-none-any.whl → 0.31.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/crackerjack.py +0 -3805
  151. crackerjack/pyproject.toml +0 -286
  152. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  153. crackerjack-0.30.3.dist-info/RECORD +0 -16
  154. {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,26 +1,18 @@
1
1
  import time
2
2
  import typing as t
3
+ from dataclasses import dataclass
3
4
  from enum import Enum, auto
4
- from pathlib import Path
5
+ from functools import partial
6
+ from typing import Protocol
5
7
 
6
- from rich.box import ROUNDED
7
8
  from rich.console import Console
8
- from rich.layout import Layout
9
- from rich.live import Live
10
9
  from rich.panel import Panel
11
- from rich.progress import (
12
- BarColumn,
13
- Progress,
14
- SpinnerColumn,
15
- TextColumn,
16
- TimeElapsedColumn,
17
- )
18
- from rich.prompt import Confirm, Prompt
10
+ from rich.prompt import Confirm
19
11
  from rich.table import Table
20
12
  from rich.text import Text
21
13
  from rich.tree import Tree
22
14
 
23
- from .errors import CrackerjackError, ErrorCode, handle_error
15
+ from .errors import CrackerjackError, ErrorCode
24
16
 
25
17
 
26
18
  class TaskStatus(Enum):
@@ -31,17 +23,94 @@ class TaskStatus(Enum):
31
23
  SKIPPED = auto()
32
24
 
33
25
 
26
+ @dataclass
27
+ class WorkflowOptions:
28
+ clean: bool = False
29
+ test: bool = False
30
+ publish: str | None = None
31
+ bump: str | None = None
32
+ commit: bool = False
33
+ create_pr: bool = False
34
+ interactive: bool = True
35
+ dry_run: bool = False
36
+
37
+ @classmethod
38
+ def from_args(cls, args: t.Any) -> "WorkflowOptions":
39
+ return cls(
40
+ clean=getattr(args, "clean", False),
41
+ test=getattr(args, "test", False),
42
+ publish=getattr(args, "publish", None),
43
+ bump=getattr(args, "bump", None),
44
+ commit=getattr(args, "commit", False),
45
+ create_pr=getattr(args, "create_pr", False),
46
+ interactive=getattr(args, "interactive", True),
47
+ dry_run=getattr(args, "dry_run", False),
48
+ )
49
+
50
+
51
+ class TaskExecutor(Protocol):
52
+ def __call__(self) -> bool: ...
53
+
54
+
55
+ @dataclass
56
+ class TaskDefinition:
57
+ id: str
58
+ name: str
59
+ description: str
60
+ dependencies: list[str]
61
+ optional: bool = False
62
+ estimated_duration: float = 0.0
63
+
64
+ def __post_init__(self) -> None:
65
+ if not self.dependencies:
66
+ self.dependencies = []
67
+
68
+
34
69
  class Task:
35
70
  def __init__(
36
- self, name: str, description: str, dependencies: list["Task"] | None = None
71
+ self,
72
+ definition: TaskDefinition,
73
+ executor: TaskExecutor | None = None,
74
+ workflow_tasks: dict[str, "Task"] | None = None,
37
75
  ) -> None:
38
- self.name = name
39
- self.description = description
40
- self.dependencies = dependencies or []
76
+ self.definition = definition
77
+ self.executor = executor
41
78
  self.status = TaskStatus.PENDING
42
79
  self.start_time: float | None = None
43
80
  self.end_time: float | None = None
44
81
  self.error: CrackerjackError | None = None
82
+ self._workflow_tasks = workflow_tasks
83
+ import logging
84
+
85
+ self.logger = logging.getLogger(f"crackerjack.task.{definition.id}")
86
+
87
+ @property
88
+ def name(self) -> str:
89
+ return self.definition.name
90
+
91
+ @property
92
+ def description(self) -> str:
93
+ return self.definition.description
94
+
95
+ @property
96
+ def dependencies(self) -> list["Task"] | list[str]:
97
+ if self._workflow_tasks:
98
+ return [
99
+ self._workflow_tasks[dep_name]
100
+ for dep_name in self.definition.dependencies
101
+ if dep_name in self._workflow_tasks
102
+ ]
103
+ return self.definition.dependencies
104
+
105
+ def get_resolved_dependencies(
106
+ self,
107
+ workflow_tasks: dict[str, "Task"],
108
+ ) -> list["Task"]:
109
+ return [
110
+ workflow_tasks[dep_name]
111
+ for dep_name in self.definition.dependencies
112
+ if dep_name in workflow_tasks
113
+ ]
45
114
 
46
115
  @property
47
116
  def duration(self) -> float | None:
@@ -53,56 +122,179 @@ class Task:
53
122
  def start(self) -> None:
54
123
  self.status = TaskStatus.RUNNING
55
124
  self.start_time = time.time()
125
+ self.logger.info("Task started", extra={"task_id": self.definition.id})
56
126
 
57
127
  def complete(self, success: bool = True) -> None:
58
128
  self.end_time = time.time()
59
129
  self.status = TaskStatus.SUCCESS if success else TaskStatus.FAILED
60
130
 
131
+ self.logger.info(
132
+ "Task completed",
133
+ extra={
134
+ "task_id": self.definition.id,
135
+ "success": success,
136
+ "duration": self.duration,
137
+ },
138
+ )
139
+
61
140
  def skip(self) -> None:
62
141
  self.status = TaskStatus.SKIPPED
142
+ self.end_time = time.time()
143
+ self.logger.info("Task skipped", extra={"task_id": self.definition.id})
63
144
 
64
145
  def fail(self, error: CrackerjackError) -> None:
65
- self.end_time = time.time()
66
146
  self.status = TaskStatus.FAILED
147
+ self.end_time = time.time()
67
148
  self.error = error
68
149
 
69
- def can_run(self) -> bool:
70
- return all(
71
- dep.status in (TaskStatus.SUCCESS, TaskStatus.SKIPPED)
72
- for dep in self.dependencies
150
+ self.logger.error(
151
+ "Task failed",
152
+ extra={
153
+ "task_id": self.definition.id,
154
+ "error": str(error),
155
+ "duration": self.duration,
156
+ },
73
157
  )
74
158
 
75
- def __str__(self) -> str:
76
- return f"{self.name} ({self.status.name})"
159
+ def can_run(self, completed_tasks: set[str]) -> bool:
160
+ if self._workflow_tasks:
161
+ resolved_deps = self.get_resolved_dependencies(self._workflow_tasks)
162
+ return all(
163
+ dep.status in (TaskStatus.SUCCESS, TaskStatus.SKIPPED)
164
+ for dep in resolved_deps
165
+ )
166
+
167
+ return all(dep in completed_tasks for dep in self.definition.dependencies)
168
+
169
+
170
+ class WorkflowBuilder:
171
+ def __init__(self, console: Console) -> None:
172
+ self.console = console
173
+ self.tasks: dict[str, TaskDefinition] = {}
174
+ import logging
175
+
176
+ self.logger = logging.getLogger("crackerjack.workflow.builder")
177
+
178
+ def add_task(
179
+ self,
180
+ task_id: str,
181
+ name: str,
182
+ description: str,
183
+ dependencies: list[str] | None = None,
184
+ optional: bool = False,
185
+ estimated_duration: float = 0.0,
186
+ ) -> "WorkflowBuilder":
187
+ task_def = TaskDefinition(
188
+ id=task_id,
189
+ name=name,
190
+ description=description,
191
+ dependencies=dependencies or [],
192
+ optional=optional,
193
+ estimated_duration=estimated_duration,
194
+ )
195
+
196
+ self.tasks[task_id] = task_def
197
+ self.logger.debug("Task added to workflow", extra={"task_id": task_id})
198
+ return self
199
+
200
+ def add_conditional_task(
201
+ self,
202
+ condition: bool,
203
+ task_id: str,
204
+ name: str,
205
+ description: str,
206
+ dependencies: list[str] | None = None,
207
+ estimated_duration: float = 0.0,
208
+ ) -> str:
209
+ if condition:
210
+ self.add_task(
211
+ task_id=task_id,
212
+ name=name,
213
+ description=description,
214
+ dependencies=dependencies,
215
+ estimated_duration=estimated_duration,
216
+ )
217
+ return task_id
218
+
219
+ return dependencies[-1] if dependencies else ""
220
+
221
+ def build(self) -> dict[str, TaskDefinition]:
222
+ self._validate_workflow()
223
+ return self.tasks.copy()
224
+
225
+ def _validate_workflow(self) -> None:
226
+ self._validate_dependencies()
227
+ self._check_circular_dependencies()
228
+
229
+ def _validate_dependencies(self) -> None:
230
+ for task_id, task_def in self.tasks.items():
231
+ for dep in task_def.dependencies:
232
+ if dep not in self.tasks:
233
+ msg = f"Task {task_id} depends on unknown task {dep}"
234
+ raise ValueError(msg)
235
+
236
+ def _check_circular_dependencies(self) -> None:
237
+ visited: set[str] = set()
238
+ rec_stack: set[str] = set()
239
+
240
+ def has_cycle(task_id: str) -> bool:
241
+ visited.add(task_id)
242
+ rec_stack.add(task_id)
243
+
244
+ for dep in self.tasks[task_id].dependencies:
245
+ if dep not in visited:
246
+ if has_cycle(dep):
247
+ return True
248
+ elif dep in rec_stack:
249
+ return True
250
+
251
+ rec_stack.remove(task_id)
252
+ return False
253
+
254
+ for task_id in self.tasks:
255
+ if task_id not in visited and has_cycle(task_id):
256
+ msg = f"Circular dependency detected involving task {task_id}"
257
+ raise ValueError(
258
+ msg,
259
+ )
77
260
 
78
261
 
79
262
  class WorkflowManager:
80
263
  def __init__(self, console: Console) -> None:
81
264
  self.console = console
82
265
  self.tasks: dict[str, Task] = {}
83
- self.current_task: Task | None = None
266
+ self.task_definitions: dict[str, TaskDefinition] = {}
267
+ import logging
84
268
 
85
- def add_task(
86
- self, name: str, description: str, dependencies: list[str] | None = None
87
- ) -> Task:
88
- dep_tasks: list[Task] = []
89
- if dependencies:
90
- for dep_name in dependencies:
91
- if dep_name not in self.tasks:
92
- raise ValueError(f"Dependency task '{dep_name}' not found")
93
- dep_tasks.append(self.tasks[dep_name])
94
- task = Task(name, description, dep_tasks)
95
- self.tasks[name] = task
96
- return task
269
+ self.logger = logging.getLogger("crackerjack.workflow.manager")
270
+
271
+ def load_workflow(self, task_definitions: dict[str, TaskDefinition]) -> None:
272
+ self.task_definitions = task_definitions
273
+ self.tasks = {
274
+ task_id: Task(definition)
275
+ for task_id, definition in task_definitions.items()
276
+ }
277
+
278
+ for task in self.tasks.values():
279
+ task._workflow_tasks = self.tasks
280
+
281
+ self.logger.info("Workflow loaded", extra={"task_count": len(self.tasks)})
282
+
283
+ def set_task_executor(self, task_id: str, executor: TaskExecutor) -> None:
284
+ if task_id in self.tasks:
285
+ self.tasks[task_id].executor = executor
97
286
 
98
287
  def get_next_task(self) -> Task | None:
288
+ completed_tasks = {
289
+ task_id
290
+ for task_id, task in self.tasks.items()
291
+ if task.status == TaskStatus.SUCCESS
292
+ }
293
+
99
294
  for task in self.tasks.values():
100
- if (
101
- task.status == TaskStatus.PENDING
102
- and task.can_run()
103
- and (task != self.current_task)
104
- ):
295
+ if task.status == TaskStatus.PENDING and task.can_run(completed_tasks):
105
296
  return task
297
+
106
298
  return None
107
299
 
108
300
  def all_tasks_completed(self) -> bool:
@@ -111,320 +303,389 @@ class WorkflowManager:
111
303
  for task in self.tasks.values()
112
304
  )
113
305
 
114
- def run_task(self, task: Task, func: t.Callable[[], t.Any]) -> bool:
115
- self.current_task = task
306
+ def run_task(self, task: Task) -> bool:
307
+ if not task.executor:
308
+ return self._handle_task_without_executor(task)
309
+
310
+ return self._execute_task_with_executor(task)
311
+
312
+ def _handle_task_without_executor(self, task: Task) -> bool:
313
+ task.skip()
314
+ self.console.print(f"[yellow]⏭️ Skipped {task.name} (no executor)[/yellow]")
315
+ return True
316
+
317
+ def _execute_task_with_executor(self, task: Task) -> bool:
116
318
  task.start()
319
+ self.console.print(f"[blue]🔄 Running {task.name}...[/blue]")
320
+
117
321
  try:
118
- func()
119
- task.complete()
120
- return True
121
- except CrackerjackError as e:
122
- task.fail(e)
123
- return False
322
+ return self._try_execute_task(task)
124
323
  except Exception as e:
125
- from .errors import ExecutionError
324
+ return self._handle_task_exception(task, e)
126
325
 
127
- error = ExecutionError(
128
- message=f"Unexpected error in task '{task.name}'",
129
- error_code=ErrorCode.UNEXPECTED_ERROR,
130
- details=str(e),
131
- recovery=f"This is an unexpected error in task '{task.name}'. Please report this issue.",
132
- )
133
- task.fail(error)
134
- return False
135
- finally:
136
- self.current_task = None
326
+ def _try_execute_task(self, task: Task) -> bool:
327
+ self.logger.info(f"Executing task: {task.definition.id}")
328
+ success = task.executor() if task.executor else False
329
+ task.complete(success)
330
+
331
+ self._display_task_result(task, success)
332
+ return success
333
+
334
+ def _display_task_result(self, task: Task, success: bool) -> None:
335
+ if success:
336
+ duration_str = f" ({task.duration: .1f}s)" if task.duration else ""
337
+ self.console.print(f"[green]✅ {task.name}{duration_str}[/green]")
338
+ else:
339
+ self.console.print(f"[red]❌ {task.name} failed[/red]")
340
+
341
+ def _handle_task_exception(self, task: Task, e: Exception) -> bool:
342
+ error = CrackerjackError(
343
+ message=f"Task {task.name} failed: {e}",
344
+ error_code=ErrorCode.COMMAND_EXECUTION_ERROR,
345
+ )
346
+ task.fail(error)
347
+ self.console.print(f"[red]💥 {task.name} crashed: {e}[/red]")
348
+ return False
137
349
 
138
350
  def display_task_tree(self) -> None:
139
- tree = Tree("Workflow")
140
- for task in self.tasks.values():
141
- if not task.dependencies:
142
- self._add_task_to_tree(task, tree)
351
+ tree = Tree("🚀 Workflow Tasks")
352
+ status_groups = self._get_status_groups()
353
+
354
+ for status, (label, color) in status_groups.items():
355
+ self._add_status_branch(tree, status, label, color)
356
+
143
357
  self.console.print(tree)
144
358
 
145
- def _add_task_to_tree(self, task: Task, parent: Tree) -> None:
146
- if task.status == TaskStatus.SUCCESS:
147
- status = "[green]✅[/green]"
148
- elif task.status == TaskStatus.FAILED:
149
- status = "[red]❌[/red]"
150
- elif task.status == TaskStatus.RUNNING:
151
- status = "[yellow][/yellow]"
152
- elif task.status == TaskStatus.SKIPPED:
153
- status = "[blue]⏩[/blue]"
154
- else:
155
- status = "[grey]⏸️[/grey]"
156
- branch = parent.add(f"{status} {task.name} - {task.description}")
157
- for dependent in self.tasks.values():
158
- if task in dependent.dependencies:
159
- self._add_task_to_tree(dependent, branch)
359
+ def _get_status_groups(self) -> dict[TaskStatus, tuple[str, str]]:
360
+ return {
361
+ TaskStatus.SUCCESS: ("✅ Completed", "green"),
362
+ TaskStatus.RUNNING: ("🔄 Running", "blue"),
363
+ TaskStatus.FAILED: ("❌ Failed", "red"),
364
+ TaskStatus.SKIPPED: ("⏭️ Skipped", "yellow"),
365
+ TaskStatus.PENDING: ("⏳ Pending", "white"),
366
+ }
367
+
368
+ def _add_status_branch(
369
+ self,
370
+ tree: Tree,
371
+ status: TaskStatus,
372
+ label: str,
373
+ color: str,
374
+ ) -> None:
375
+ status_tasks = [task for task in self.tasks.values() if task.status == status]
376
+
377
+ if not status_tasks:
378
+ return
379
+
380
+ status_branch = tree.add(f"[{color}]{label}[/{color}]")
381
+ for task in status_tasks:
382
+ duration_str = f" ({task.duration: .1f}s)" if task.duration else ""
383
+ status_branch.add(f"{task.name}{duration_str}")
384
+
385
+ def get_workflow_summary(self) -> dict[str, int]:
386
+ summary = {status.name.lower(): 0 for status in TaskStatus}
387
+
388
+ for task in self.tasks.values():
389
+ summary[task.status.name.lower()] += 1
390
+
391
+ return summary
160
392
 
161
393
 
162
394
  class InteractiveCLI:
163
395
  def __init__(self, console: Console | None = None) -> None:
164
396
  self.console = console or Console()
165
397
  self.workflow = WorkflowManager(self.console)
398
+ import logging
399
+
400
+ self.logger = logging.getLogger("crackerjack.interactive.cli")
401
+
402
+ def create_dynamic_workflow(self, options: WorkflowOptions) -> None:
403
+ builder = WorkflowBuilder(self.console)
404
+
405
+ workflow_steps = [
406
+ self._add_setup_phase,
407
+ self._add_config_phase,
408
+ partial(self._add_cleaning_phase, enabled=options.clean),
409
+ self._add_fast_hooks_phase,
410
+ partial(self._add_testing_phase, enabled=options.test),
411
+ self._add_comprehensive_hooks_phase,
412
+ partial(
413
+ self._add_version_phase,
414
+ enabled=bool(options.publish or options.bump),
415
+ ),
416
+ partial(self._add_publish_phase, enabled=bool(options.publish)),
417
+ partial(self._add_commit_phase, enabled=options.commit),
418
+ partial(self._add_pr_phase, enabled=options.create_pr),
419
+ ]
420
+
421
+ last_task = ""
422
+ for step in workflow_steps:
423
+ last_task = step(builder, last_task)
166
424
 
167
- def show_banner(self, version: str) -> None:
168
- title = Text("Crackerjack", style="bold cyan")
169
- version_text = Text(f"v{version}", style="dim cyan")
170
- subtitle = Text("Your Python project management toolkit", style="italic")
171
- panel = Panel(
172
- f"{title} {version_text}\n{subtitle}", border_style="cyan", expand=False
425
+ workflow_def = builder.build()
426
+ self.workflow.load_workflow(workflow_def)
427
+
428
+ self.logger.info(
429
+ "Dynamic workflow created",
430
+ extra={"task_count": len(workflow_def)},
173
431
  )
174
- self.console.print(panel)
175
- self.console.print()
176
432
 
177
- def create_standard_workflow(self) -> None:
178
- self.workflow.add_task("setup", "Initialize project structure")
179
- self.workflow.add_task(
180
- "config", "Update configuration files", dependencies=["setup"]
433
+ def _add_setup_phase(self, builder: WorkflowBuilder, last_task: str) -> str:
434
+ builder.add_task(
435
+ "setup",
436
+ "Initialize",
437
+ "Initialize project structure",
438
+ estimated_duration=2.0,
181
439
  )
182
- self.workflow.add_task(
183
- "clean", "Clean code (remove docstrings, comments)", dependencies=["config"]
440
+ return "setup"
441
+
442
+ def _add_config_phase(self, builder: WorkflowBuilder, last_task: str) -> str:
443
+ builder.add_task(
444
+ "config",
445
+ "Configure",
446
+ "Update configuration files",
447
+ dependencies=[last_task],
448
+ estimated_duration=3.0,
184
449
  )
185
- self.workflow.add_task("hooks", "Run pre-commit hooks", dependencies=["clean"])
186
- self.workflow.add_task("test", "Run tests", dependencies=["hooks"])
187
- self.workflow.add_task("version", "Bump version", dependencies=["test"])
188
- self.workflow.add_task("publish", "Publish package", dependencies=["version"])
189
- self.workflow.add_task("commit", "Commit changes", dependencies=["publish"])
190
-
191
- def setup_layout(self) -> Layout:
192
- layout = Layout()
193
- layout.split(
194
- Layout(name="header", size=3),
195
- Layout(name="main"),
196
- Layout(name="footer", size=3),
450
+ return "config"
451
+
452
+ def _add_cleaning_phase(
453
+ self,
454
+ builder: WorkflowBuilder,
455
+ last_task: str,
456
+ enabled: bool,
457
+ ) -> str:
458
+ return (
459
+ builder.add_conditional_task(
460
+ condition=enabled,
461
+ task_id="clean",
462
+ name="Clean Code",
463
+ description="Clean code (remove docstrings, comments)",
464
+ dependencies=[last_task],
465
+ estimated_duration=10.0,
466
+ )
467
+ or last_task
197
468
  )
198
- layout["main"].split_row(Layout(name="tasks"), Layout(name="details", ratio=2))
199
- return layout
200
-
201
- def show_task_status(self, task: Task) -> Panel:
202
- if task.status == TaskStatus.RUNNING:
203
- status = "[yellow]Running[/yellow]"
204
- style = "yellow"
205
- elif task.status == TaskStatus.SUCCESS:
206
- status = "[green]Success[/green]"
207
- style = "green"
208
- elif task.status == TaskStatus.FAILED:
209
- status = "[red]Failed[/red]"
210
- style = "red"
211
- elif task.status == TaskStatus.SKIPPED:
212
- status = "[blue]Skipped[/blue]"
213
- style = "blue"
214
- else:
215
- status = "[dim white]Pending[/dim white]"
216
- style = "dim"
217
- duration = task.duration
218
- duration_text = f"Duration: {duration:.2f}s" if duration else ""
219
- content = f"{task.name}: {task.description}\nStatus: {status}\n{duration_text}"
220
- if task.error:
221
- content += f"\n[red]Error: {task.error.message}[/red]"
222
- if task.error.details:
223
- content += f"\n[dim red]Details: {task.error.details}[/dim red]"
224
- if task.error.recovery:
225
- content += f"\n[yellow]Recovery: {task.error.recovery}[/yellow]"
226
- return Panel(content, title=task.name, border_style=style, expand=False)
227
-
228
- def show_task_table(self) -> Table:
229
- table = Table(
230
- title="Workflow Tasks",
231
- header_style="bold white",
469
+
470
+ def _add_fast_hooks_phase(self, builder: WorkflowBuilder, last_task: str) -> str:
471
+ builder.add_task(
472
+ "fast_hooks",
473
+ "Format",
474
+ "Run formatting hooks",
475
+ dependencies=[last_task],
476
+ estimated_duration=15.0,
477
+ )
478
+ return "fast_hooks"
479
+
480
+ def _add_testing_phase(
481
+ self,
482
+ builder: WorkflowBuilder,
483
+ last_task: str,
484
+ enabled: bool,
485
+ ) -> str:
486
+ return (
487
+ builder.add_conditional_task(
488
+ condition=enabled,
489
+ task_id="test",
490
+ name="Test",
491
+ description="Run tests with coverage",
492
+ dependencies=[last_task],
493
+ estimated_duration=30.0,
494
+ )
495
+ or last_task
232
496
  )
233
- table.add_column("Task", style="white")
234
- table.add_column("Status")
235
- table.add_column("Duration")
236
- table.add_column("Dependencies")
237
- for task in self.workflow.tasks.values():
238
- if task.status == TaskStatus.RUNNING:
239
- status = "[yellow]Running[/yellow]"
240
- elif task.status == TaskStatus.SUCCESS:
241
- status = "[green]Success[/green]"
242
- elif task.status == TaskStatus.FAILED:
243
- status = "[red]Failed[/red]"
244
- elif task.status == TaskStatus.SKIPPED:
245
- status = "[blue]Skipped[/blue]"
246
- else:
247
- status = "[dim white]Pending[/dim white]"
248
- duration = task.duration
249
- duration_text = f"{duration:.2f}s" if duration else "-"
250
- deps = ", ".join(dep.name for dep in task.dependencies) or "-"
251
- table.add_row(task.name, status, duration_text, deps)
252
- return table
253
-
254
- def run_interactive(self) -> None:
255
- self.console.clear()
256
- layout = self._setup_interactive_layout()
257
- progress_tracker = self._create_progress_tracker()
258
- with Live(layout) as live:
259
- try:
260
- self._execute_workflow_loop(layout, progress_tracker, live)
261
- self._display_final_summary(layout)
262
- except KeyboardInterrupt:
263
- self._handle_user_interruption(layout)
264
- self.console.print("\nWorkflow Status:")
265
- self.workflow.display_task_tree()
266
497
 
267
- def _setup_interactive_layout(self) -> Layout:
268
- layout = self.setup_layout()
269
- layout["header"].update(
270
- Panel("Crackerjack Interactive Mode", style="bold white")
498
+ def _add_comprehensive_hooks_phase(
499
+ self,
500
+ builder: WorkflowBuilder,
501
+ last_task: str,
502
+ ) -> str:
503
+ builder.add_task(
504
+ "comprehensive_hooks",
505
+ "Quality Check",
506
+ "Run comprehensive hooks",
507
+ dependencies=[last_task],
508
+ estimated_duration=45.0,
271
509
  )
272
- layout["footer"].update(Panel("Press Ctrl+C to exit", style="dim"))
273
- return layout
274
-
275
- def _create_progress_tracker(self) -> dict[str, t.Any]:
276
- progress = Progress(
277
- SpinnerColumn(),
278
- TextColumn("[white]{task.description}"),
279
- BarColumn(),
280
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
281
- TimeElapsedColumn(),
510
+ return "comprehensive_hooks"
511
+
512
+ def _add_version_phase(
513
+ self,
514
+ builder: WorkflowBuilder,
515
+ last_task: str,
516
+ enabled: bool,
517
+ ) -> str:
518
+ return (
519
+ builder.add_conditional_task(
520
+ condition=enabled,
521
+ task_id="version",
522
+ name="Version",
523
+ description="Bump version",
524
+ dependencies=[last_task],
525
+ estimated_duration=5.0,
526
+ )
527
+ or last_task
282
528
  )
283
- total_tasks = len(self.workflow.tasks)
284
- progress_task = progress.add_task("Running workflow", total=total_tasks)
285
- return {
286
- "progress": progress,
287
- "progress_task": progress_task,
288
- "completed_tasks": 0,
289
- }
290
529
 
291
- def _execute_workflow_loop(
292
- self, layout: Layout, progress_tracker: dict[str, t.Any], live: Live
293
- ) -> None:
530
+ def _add_publish_phase(
531
+ self,
532
+ builder: WorkflowBuilder,
533
+ last_task: str,
534
+ enabled: bool,
535
+ ) -> str:
536
+ return (
537
+ builder.add_conditional_task(
538
+ condition=enabled,
539
+ task_id="publish",
540
+ name="Publish",
541
+ description="Publish package",
542
+ dependencies=[last_task],
543
+ estimated_duration=20.0,
544
+ )
545
+ or last_task
546
+ )
547
+
548
+ def _add_commit_phase(
549
+ self,
550
+ builder: WorkflowBuilder,
551
+ last_task: str,
552
+ enabled: bool,
553
+ ) -> str:
554
+ return (
555
+ builder.add_conditional_task(
556
+ condition=enabled,
557
+ task_id="commit",
558
+ name="Commit",
559
+ description="Commit changes",
560
+ dependencies=[last_task],
561
+ estimated_duration=3.0,
562
+ )
563
+ or last_task
564
+ )
565
+
566
+ def _add_pr_phase(
567
+ self,
568
+ builder: WorkflowBuilder,
569
+ last_task: str,
570
+ enabled: bool,
571
+ ) -> str:
572
+ return (
573
+ builder.add_conditional_task(
574
+ condition=enabled,
575
+ task_id="pr",
576
+ name="Pull Request",
577
+ description="Create pull request",
578
+ dependencies=[last_task],
579
+ estimated_duration=5.0,
580
+ )
581
+ or last_task
582
+ )
583
+
584
+ def run_interactive_workflow(self, options: WorkflowOptions) -> bool:
585
+ self.logger.info(
586
+ f"Starting interactive workflow with options: {options.__dict__}",
587
+ )
588
+ self.create_dynamic_workflow(options)
589
+
590
+ self.console.print("[bold blue]🚀 Starting Interactive Workflow[/bold blue]")
591
+ self.workflow.display_task_tree()
592
+
593
+ if not Confirm.ask("Continue with workflow?"):
594
+ self.console.print("[yellow]Workflow cancelled by user[/yellow]")
595
+ return False
596
+
597
+ return self._execute_workflow_loop()
598
+
599
+ def _execute_workflow_loop(self) -> bool:
600
+ overall_success = True
601
+
294
602
  while not self.workflow.all_tasks_completed():
295
- layout["tasks"].update(self.show_task_table())
296
603
  next_task = self.workflow.get_next_task()
297
- if not next_task:
604
+
605
+ if next_task is None:
606
+ overall_success = self._handle_stuck_workflow()
298
607
  break
299
- if self._should_execute_task(layout, next_task, live):
300
- self._execute_task(layout, next_task, progress_tracker)
301
- else:
302
- next_task.skip()
303
-
304
- def _should_execute_task(self, layout: Layout, task: Task, live: Live) -> bool:
305
- layout["details"].update(self.show_task_status(task))
306
- live.stop()
307
- should_run = Confirm.ask(f"Run task '{task.name}'?", default=True)
308
- live.start()
309
- return should_run
310
-
311
- def _execute_task(
312
- self, layout: Layout, task: Task, progress_tracker: dict[str, t.Any]
313
- ) -> None:
314
- task.start()
315
- layout["details"].update(self.show_task_status(task))
316
- time.sleep(1)
317
- success = self._simulate_task_execution()
318
- if success:
319
- task.complete()
320
- progress_tracker["completed_tasks"] += 1
321
- else:
322
- error = self._create_task_error(task.name)
323
- task.fail(error)
324
- progress_tracker["progress"].update(
325
- progress_tracker["progress_task"],
326
- completed=progress_tracker["completed_tasks"],
327
- )
328
- layout["details"].update(self.show_task_status(task))
329
608
 
330
- def _simulate_task_execution(self) -> bool:
331
- import random
609
+ if not self._should_run_task(next_task):
610
+ continue
332
611
 
333
- return random.choice([True, True, True, False])
612
+ success = self._execute_single_task(next_task)
613
+ if not success:
614
+ overall_success = False
615
+ if not self._should_continue_after_failure():
616
+ break
334
617
 
335
- def _create_task_error(self, task_name: str) -> t.Any:
336
- from .errors import ExecutionError
618
+ self._display_workflow_summary()
619
+ return overall_success
337
620
 
338
- return ExecutionError(
339
- message=f"Task '{task_name}' failed",
340
- error_code=ErrorCode.COMMAND_EXECUTION_ERROR,
341
- details="This is a simulated failure for demonstration.",
342
- recovery=f"Retry the '{task_name}' task.",
343
- )
621
+ def _handle_stuck_workflow(self) -> bool:
622
+ pending_tasks = [
623
+ task
624
+ for task in self.workflow.tasks.values()
625
+ if task.status == TaskStatus.PENDING
626
+ ]
344
627
 
345
- def _display_final_summary(self, layout: Layout) -> None:
346
- layout["tasks"].update(self.show_task_table())
347
- task_counts = self._count_tasks_by_status()
348
- summary = Panel(
349
- f"🏆 Workflow completed!\n\n"
350
- f"[green]✅ Successful tasks: {task_counts['successful']}[/green]\n"
351
- f"[red]❌ Failed tasks: {task_counts['failed']}[/red]\n"
352
- f"[blue]⏩ Skipped tasks: {task_counts['skipped']}[/blue]",
353
- title="Summary",
354
- border_style="cyan",
355
- )
356
- layout["details"].update(summary)
628
+ if pending_tasks:
629
+ self.console.print("[red]❌ Workflow stuck - unresolved dependencies[/red]")
630
+ return False
631
+ return True
357
632
 
358
- def _count_tasks_by_status(self) -> dict[str, int]:
359
- return {
360
- "successful": sum(
361
- 1
362
- for task in self.workflow.tasks.values()
363
- if task.status == TaskStatus.SUCCESS
364
- ),
365
- "failed": sum(
366
- 1
367
- for task in self.workflow.tasks.values()
368
- if task.status == TaskStatus.FAILED
369
- ),
370
- "skipped": sum(
371
- 1
372
- for task in self.workflow.tasks.values()
373
- if task.status == TaskStatus.SKIPPED
374
- ),
633
+ def _should_run_task(self, task: Task) -> bool:
634
+ if not Confirm.ask(f"Run {task.name}?", default=True):
635
+ task.skip()
636
+ return False
637
+ return True
638
+
639
+ def _execute_single_task(self, task: Task) -> bool:
640
+ return self.workflow.run_task(task)
641
+
642
+ def _should_continue_after_failure(self) -> bool:
643
+ return Confirm.ask("Continue despite failure?", default=True)
644
+
645
+ def _display_workflow_summary(self) -> None:
646
+ summary = self.workflow.get_workflow_summary()
647
+
648
+ self.console.print("\n[bold]📊 Workflow Summary[/bold]")
649
+
650
+ table = Table(show_header=True, header_style="bold magenta")
651
+ table.add_column("Status", style="cyan")
652
+ table.add_column("Count", justify="right")
653
+
654
+ status_styles = {
655
+ "success": "green",
656
+ "failed": "red",
657
+ "skipped": "yellow",
658
+ "pending": "white",
375
659
  }
376
660
 
377
- def _handle_user_interruption(self, layout: Layout) -> None:
378
- layout["footer"].update(Panel("Interrupted by user", style="yellow"))
379
-
380
- def ask_for_file(
381
- self, prompt: str, directory: Path, default: str | None = None
382
- ) -> Path:
383
- self.console.print(f"\n[bold]{prompt}[/bold]")
384
- files = list(directory.iterdir())
385
- files.sort()
386
- table = Table(title=f"Files in {directory}", box=ROUNDED)
387
- table.add_column("#", style="cyan")
388
- table.add_column("Filename", style="green")
389
- table.add_column("Size", style="blue")
390
- table.add_column("Modified", style="yellow")
391
- for i, file in enumerate(files, 1):
392
- if file.is_file():
393
- size = f"{file.stat().st_size / 1024:.1f} KB"
394
- mtime = time.strftime(
395
- "%Y-%m-%d %H:%M:%S", time.localtime(file.stat().st_mtime)
396
- )
397
- table.add_row(str(i), file.name, size, mtime)
398
- self.console.print(table)
399
- selection = Prompt.ask("Enter file number or name", default=default or "")
400
- if selection.isdigit() and 1 <= int(selection) <= len(files):
401
- return files[int(selection) - 1]
402
- else:
403
- for file in files:
404
- if file.name == selection:
405
- return file
406
- return directory / selection
407
-
408
- def confirm_dangerous_action(self, action: str, details: str) -> bool:
409
- panel = Panel(
410
- f"[bold red]WARNING: {action}[/bold red]\n\n{details}\n\nThis action cannot be undone. Please type the action name to confirm.",
411
- title="Confirmation Required",
412
- border_style="red",
413
- )
414
- self.console.print(panel)
415
- confirmation = Prompt.ask("Type the action name to confirm")
416
- return confirmation.lower() == action.lower()
661
+ for status, count in summary.items():
662
+ if count > 0:
663
+ style = status_styles.get(status, "white")
664
+ table.add_row(f"[{style}]{status.title()}[/{style}]", str(count))
417
665
 
418
- def show_error(self, error: CrackerjackError, verbose: bool = False) -> None:
419
- handle_error(error, self.console, verbose, exit_on_error=False)
666
+ self.console.print(table)
420
667
 
421
668
 
422
- def launch_interactive_cli(version: str) -> None:
669
+ def launch_interactive_cli(version: str, options: t.Any = None) -> None:
423
670
  console = Console()
424
671
  cli = InteractiveCLI(console)
425
- cli.show_banner(version)
426
- cli.create_standard_workflow()
427
- cli.run_interactive()
672
+
673
+ title = Text("Crackerjack", style="bold cyan")
674
+ version_text = Text(f"v{version}", style="dim cyan")
675
+ subtitle = Text("Your Python project management toolkit", style="italic")
676
+ panel = Panel(
677
+ f"{title} {version_text}\n{subtitle}",
678
+ border_style="cyan",
679
+ expand=False,
680
+ )
681
+ console.print(panel)
682
+ console.print()
683
+
684
+ workflow_options = (
685
+ WorkflowOptions.from_args(options) if options else WorkflowOptions()
686
+ )
687
+ cli.create_dynamic_workflow(workflow_options)
688
+ cli.run_interactive_workflow(workflow_options)
428
689
 
429
690
 
430
691
  if __name__ == "__main__":