crackerjack 0.29.0__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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
crackerjack/interactive.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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,
|
|
71
|
+
self,
|
|
72
|
+
definition: TaskDefinition,
|
|
73
|
+
executor: TaskExecutor | None = None,
|
|
74
|
+
workflow_tasks: dict[str, "Task"] | None = None,
|
|
37
75
|
) -> None:
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
76
|
-
|
|
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.
|
|
266
|
+
self.task_definitions: dict[str, TaskDefinition] = {}
|
|
267
|
+
import logging
|
|
84
268
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
) ->
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
task
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
+
return self._handle_task_exception(task, e)
|
|
126
325
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
self
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
292
|
-
self,
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
609
|
+
if not self._should_run_task(next_task):
|
|
610
|
+
continue
|
|
332
611
|
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
618
|
+
self._display_workflow_summary()
|
|
619
|
+
return overall_success
|
|
337
620
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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__":
|