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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -299
- 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 +618 -928
- 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 +94 -103
- 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/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import typing as t
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from crackerjack.code_cleaner import CodeCleaner
|
|
9
|
+
from crackerjack.models.protocols import (
|
|
10
|
+
FileSystemInterface,
|
|
11
|
+
GitInterface,
|
|
12
|
+
HookManager,
|
|
13
|
+
OptionsProtocol,
|
|
14
|
+
PublishManager,
|
|
15
|
+
TestManagerProtocol,
|
|
16
|
+
)
|
|
17
|
+
from crackerjack.services.config import ConfigurationService
|
|
18
|
+
|
|
19
|
+
from .session_coordinator import SessionCoordinator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PhaseCoordinator:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
console: Console,
|
|
26
|
+
pkg_path: Path,
|
|
27
|
+
session: SessionCoordinator,
|
|
28
|
+
filesystem: FileSystemInterface,
|
|
29
|
+
git_service: GitInterface,
|
|
30
|
+
hook_manager: HookManager,
|
|
31
|
+
test_manager: TestManagerProtocol,
|
|
32
|
+
publish_manager: PublishManager,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.console = console
|
|
35
|
+
self.pkg_path = pkg_path
|
|
36
|
+
self.session = session
|
|
37
|
+
|
|
38
|
+
self.filesystem = filesystem
|
|
39
|
+
self.git_service = git_service
|
|
40
|
+
self.hook_manager = hook_manager
|
|
41
|
+
self.test_manager = test_manager
|
|
42
|
+
self.publish_manager = publish_manager
|
|
43
|
+
|
|
44
|
+
self.code_cleaner = CodeCleaner(console=console)
|
|
45
|
+
self.config_service = ConfigurationService(console=console, pkg_path=pkg_path)
|
|
46
|
+
|
|
47
|
+
self.logger = logging.getLogger("crackerjack.phases")
|
|
48
|
+
|
|
49
|
+
def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
|
|
50
|
+
if not options.clean:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
self.session.track_task("cleaning", "Code cleaning")
|
|
54
|
+
try:
|
|
55
|
+
self._display_cleaning_header()
|
|
56
|
+
return self._execute_cleaning_process()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.console.print(f"[red]❌[/red] Cleaning failed: {e}")
|
|
59
|
+
self.session.fail_task("cleaning", str(e))
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def _display_cleaning_header(self) -> None:
|
|
63
|
+
self.console.print("\n" + " - " * 80)
|
|
64
|
+
self.console.print(
|
|
65
|
+
"[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]",
|
|
66
|
+
)
|
|
67
|
+
self.console.print(" - " * 80 + "\n")
|
|
68
|
+
self.console.print("[yellow]🧹[/yellow] Starting code cleaning...")
|
|
69
|
+
|
|
70
|
+
def _execute_cleaning_process(self) -> bool:
|
|
71
|
+
python_files = list(self.pkg_path.rglob("*.py"))
|
|
72
|
+
|
|
73
|
+
if not python_files:
|
|
74
|
+
return self._handle_no_files_to_clean()
|
|
75
|
+
|
|
76
|
+
cleaned_files = self._clean_python_files(python_files)
|
|
77
|
+
self._report_cleaning_results(cleaned_files)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def _handle_no_files_to_clean(self) -> bool:
|
|
81
|
+
self.console.print("[yellow]⚠️[/yellow] No Python files found to clean")
|
|
82
|
+
self.session.complete_task("cleaning", "No files to clean")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def _clean_python_files(self, python_files: list[Path]) -> list[str]:
|
|
86
|
+
cleaned_files: list[str] = []
|
|
87
|
+
for file_path in python_files:
|
|
88
|
+
if self.code_cleaner.should_process_file(file_path):
|
|
89
|
+
if self.code_cleaner.clean_file(file_path):
|
|
90
|
+
cleaned_files.append(str(file_path))
|
|
91
|
+
return cleaned_files
|
|
92
|
+
|
|
93
|
+
def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
|
|
94
|
+
if cleaned_files:
|
|
95
|
+
self.console.print(f"[green]✅[/green] Cleaned {len(cleaned_files)} files")
|
|
96
|
+
self.session.complete_task(
|
|
97
|
+
"cleaning",
|
|
98
|
+
f"Cleaned {len(cleaned_files)} files",
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
self.console.print("[green]✅[/green] No cleaning needed")
|
|
102
|
+
self.session.complete_task("cleaning", "No cleaning needed")
|
|
103
|
+
|
|
104
|
+
def run_configuration_phase(self, options: OptionsProtocol) -> bool:
|
|
105
|
+
if options.no_config_updates:
|
|
106
|
+
return True
|
|
107
|
+
self.session.track_task("configuration", "Configuration updates")
|
|
108
|
+
try:
|
|
109
|
+
success = True
|
|
110
|
+
|
|
111
|
+
# Check if we're running from the crackerjack project root
|
|
112
|
+
if self._is_crackerjack_project():
|
|
113
|
+
if not self._copy_config_files_to_package():
|
|
114
|
+
success = False
|
|
115
|
+
|
|
116
|
+
if not self.config_service.update_precommit_config(options):
|
|
117
|
+
success = False
|
|
118
|
+
if not self.config_service.update_pyproject_config(options):
|
|
119
|
+
success = False
|
|
120
|
+
self.session.complete_task(
|
|
121
|
+
"configuration",
|
|
122
|
+
"Configuration updated successfully"
|
|
123
|
+
if success
|
|
124
|
+
else "Some configuration updates failed",
|
|
125
|
+
)
|
|
126
|
+
return success
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.session.fail_task("configuration", str(e))
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def _is_crackerjack_project(self) -> bool:
|
|
132
|
+
"""Check if we're running from the crackerjack project root."""
|
|
133
|
+
# Check for crackerjack-specific markers
|
|
134
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
135
|
+
if not pyproject_path.exists():
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
import tomllib
|
|
140
|
+
|
|
141
|
+
with pyproject_path.open("rb") as f:
|
|
142
|
+
data = tomllib.load(f)
|
|
143
|
+
|
|
144
|
+
# Check if this is the crackerjack project
|
|
145
|
+
project_name = data.get("project", {}).get("name", "")
|
|
146
|
+
return project_name == "crackerjack"
|
|
147
|
+
except Exception:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _copy_config_files_to_package(self) -> bool:
|
|
151
|
+
"""Copy configuration files from project root to package root."""
|
|
152
|
+
try:
|
|
153
|
+
# Files to copy from project root to package root
|
|
154
|
+
files_to_copy = [
|
|
155
|
+
"pyproject.toml",
|
|
156
|
+
".pre-commit-config.yaml",
|
|
157
|
+
"CLAUDE.md",
|
|
158
|
+
"RULES.md",
|
|
159
|
+
".gitignore",
|
|
160
|
+
"mcp.json",
|
|
161
|
+
"uv.lock",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
package_dir = self.pkg_path / "crackerjack"
|
|
165
|
+
if not package_dir.exists():
|
|
166
|
+
self.console.print(
|
|
167
|
+
"[yellow]⚠️[/yellow] Package directory not found: crackerjack/",
|
|
168
|
+
)
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
copied_count = 0
|
|
172
|
+
for filename in files_to_copy:
|
|
173
|
+
src_path = self.pkg_path / filename
|
|
174
|
+
if src_path.exists():
|
|
175
|
+
dst_path = package_dir / filename
|
|
176
|
+
try:
|
|
177
|
+
import shutil
|
|
178
|
+
|
|
179
|
+
shutil.copy2(src_path, dst_path)
|
|
180
|
+
copied_count += 1
|
|
181
|
+
self.logger.debug(f"Copied {filename} to package directory")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.console.print(
|
|
184
|
+
f"[yellow]⚠️[/yellow] Failed to copy {filename}: {e}",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if copied_count > 0:
|
|
188
|
+
self.console.print(
|
|
189
|
+
f"[green]✅[/green] Copied {copied_count} config files to package directory",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
except Exception as e:
|
|
194
|
+
self.console.print(
|
|
195
|
+
f"[red]❌[/red] Failed to copy config files to package: {e}",
|
|
196
|
+
)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
def run_hooks_phase(self, options: OptionsProtocol) -> bool:
|
|
200
|
+
if options.skip_hooks:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
temp_config = self.config_service.get_temp_config_path()
|
|
204
|
+
if temp_config:
|
|
205
|
+
self.hook_manager.set_config_path(temp_config)
|
|
206
|
+
|
|
207
|
+
if not self.run_fast_hooks_only(options):
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
return self.run_comprehensive_hooks_only(options)
|
|
211
|
+
|
|
212
|
+
def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
213
|
+
if options.skip_hooks:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
return self._execute_hooks_with_retry(
|
|
217
|
+
"fast",
|
|
218
|
+
self.hook_manager.run_fast_hooks,
|
|
219
|
+
options,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
223
|
+
if options.skip_hooks:
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
return self._execute_hooks_with_retry(
|
|
227
|
+
"comprehensive",
|
|
228
|
+
self.hook_manager.run_comprehensive_hooks,
|
|
229
|
+
options,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def run_testing_phase(self, options: OptionsProtocol) -> bool:
|
|
233
|
+
if not options.test:
|
|
234
|
+
return True
|
|
235
|
+
self.session.track_task("testing", "Test execution")
|
|
236
|
+
try:
|
|
237
|
+
self.console.print("\n" + "-" * 80)
|
|
238
|
+
self.console.print(
|
|
239
|
+
"[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
|
|
240
|
+
)
|
|
241
|
+
self.console.print("-" * 80 + "\n")
|
|
242
|
+
if not self.test_manager.validate_test_environment():
|
|
243
|
+
self.session.fail_task("testing", "Test environment validation failed")
|
|
244
|
+
return False
|
|
245
|
+
test_success = self.test_manager.run_tests(options)
|
|
246
|
+
if test_success:
|
|
247
|
+
coverage_info = self.test_manager.get_coverage()
|
|
248
|
+
self.session.complete_task(
|
|
249
|
+
"testing",
|
|
250
|
+
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
self.session.fail_task("testing", "Tests failed")
|
|
254
|
+
|
|
255
|
+
return test_success
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self.console.print(f"Testing error: {e}")
|
|
258
|
+
self.session.fail_task("testing", str(e))
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def run_publishing_phase(self, options: OptionsProtocol) -> bool:
|
|
262
|
+
version_type = self._determine_version_type(options)
|
|
263
|
+
if not version_type:
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
self.session.track_task("publishing", f"Publishing ({version_type})")
|
|
267
|
+
try:
|
|
268
|
+
return self._execute_publishing_workflow(options, version_type)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self.console.print(f"[red]❌[/red] Publishing failed: {e}")
|
|
271
|
+
self.session.fail_task("publishing", str(e))
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def _determine_version_type(self, options: OptionsProtocol) -> str | None:
|
|
275
|
+
if options.publish:
|
|
276
|
+
return options.publish
|
|
277
|
+
if options.all:
|
|
278
|
+
return options.all
|
|
279
|
+
if options.bump:
|
|
280
|
+
self._handle_version_bump_only(options.bump)
|
|
281
|
+
return None
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def _execute_publishing_workflow(
|
|
285
|
+
self,
|
|
286
|
+
options: OptionsProtocol,
|
|
287
|
+
version_type: str,
|
|
288
|
+
) -> bool:
|
|
289
|
+
new_version = self.publish_manager.bump_version(version_type)
|
|
290
|
+
|
|
291
|
+
if not options.no_git_tags:
|
|
292
|
+
self.publish_manager.create_git_tag(new_version)
|
|
293
|
+
|
|
294
|
+
if self.publish_manager.publish_package():
|
|
295
|
+
self._handle_successful_publish(options, new_version)
|
|
296
|
+
return True
|
|
297
|
+
self.session.fail_task("publishing", "Package publishing failed")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def _handle_successful_publish(
|
|
301
|
+
self,
|
|
302
|
+
options: OptionsProtocol,
|
|
303
|
+
new_version: str,
|
|
304
|
+
) -> None:
|
|
305
|
+
self.console.print(f"[green]🚀[/green] Successfully published {new_version}!")
|
|
306
|
+
|
|
307
|
+
if options.cleanup_pypi:
|
|
308
|
+
self.publish_manager.cleanup_old_releases(options.keep_releases)
|
|
309
|
+
|
|
310
|
+
self.session.complete_task("publishing", f"Published {new_version}")
|
|
311
|
+
|
|
312
|
+
def run_commit_phase(self, options: OptionsProtocol) -> bool:
|
|
313
|
+
if not options.commit:
|
|
314
|
+
return True
|
|
315
|
+
self.session.track_task("commit", "Git commit and push")
|
|
316
|
+
try:
|
|
317
|
+
changed_files = self.git_service.get_changed_files()
|
|
318
|
+
if not changed_files:
|
|
319
|
+
return self._handle_no_changes_to_commit()
|
|
320
|
+
commit_message = self._get_commit_message(changed_files, options)
|
|
321
|
+
return self._execute_commit_and_push(changed_files, commit_message)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.console.print(f"[red]❌[/red] Commit failed: {e}")
|
|
324
|
+
self.session.fail_task("commit", str(e))
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
def _handle_no_changes_to_commit(self) -> bool:
|
|
328
|
+
self.console.print("[yellow]ℹ️[/yellow] No changes to commit")
|
|
329
|
+
self.session.complete_task("commit", "No changes to commit")
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def _execute_commit_and_push(
|
|
333
|
+
self,
|
|
334
|
+
changed_files: list[str],
|
|
335
|
+
commit_message: str,
|
|
336
|
+
) -> bool:
|
|
337
|
+
if not self.git_service.add_files(changed_files):
|
|
338
|
+
self.session.fail_task("commit", "Failed to stage files")
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
if not self.git_service.commit(commit_message):
|
|
342
|
+
self.session.fail_task("commit", "Commit failed")
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
return self._handle_push_result(commit_message)
|
|
346
|
+
|
|
347
|
+
def _handle_push_result(self, commit_message: str) -> bool:
|
|
348
|
+
if self.git_service.push():
|
|
349
|
+
self.console.print(
|
|
350
|
+
f"[green]🎉[/green] Committed and pushed: {commit_message}",
|
|
351
|
+
)
|
|
352
|
+
self.session.complete_task(
|
|
353
|
+
"commit",
|
|
354
|
+
f"Committed and pushed: {commit_message}",
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
self.console.print(
|
|
358
|
+
f"[yellow]⚠️[/yellow] Committed but push failed: {commit_message}",
|
|
359
|
+
)
|
|
360
|
+
self.session.complete_task(
|
|
361
|
+
"commit",
|
|
362
|
+
f"Committed (push failed): {commit_message}",
|
|
363
|
+
)
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
def execute_hooks_with_retry(
|
|
367
|
+
self,
|
|
368
|
+
hook_type: str,
|
|
369
|
+
hook_runner: t.Callable[[], list[t.Any]],
|
|
370
|
+
options: OptionsProtocol,
|
|
371
|
+
) -> bool:
|
|
372
|
+
return self._execute_hooks_with_retry(hook_type, hook_runner, options)
|
|
373
|
+
|
|
374
|
+
def _handle_version_bump_only(self, bump_type: str) -> bool:
|
|
375
|
+
self.session.track_task("version_bump", f"Version bump ({bump_type})")
|
|
376
|
+
try:
|
|
377
|
+
new_version = self.publish_manager.bump_version(bump_type)
|
|
378
|
+
self.console.print(f"[green]🎯[/green] Version bumped to {new_version}")
|
|
379
|
+
self.session.complete_task("version_bump", f"Bumped to {new_version}")
|
|
380
|
+
return True
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self.console.print(f"[red]❌[/red] Version bump failed: {e}")
|
|
383
|
+
self.session.fail_task("version_bump", str(e))
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
def _get_commit_message(
|
|
387
|
+
self,
|
|
388
|
+
changed_files: list[str],
|
|
389
|
+
options: OptionsProtocol,
|
|
390
|
+
) -> str:
|
|
391
|
+
suggestions = self.git_service.get_commit_message_suggestions(changed_files)
|
|
392
|
+
|
|
393
|
+
if not suggestions:
|
|
394
|
+
return "Update project files"
|
|
395
|
+
|
|
396
|
+
if not options.interactive:
|
|
397
|
+
return suggestions[0]
|
|
398
|
+
|
|
399
|
+
return self._interactive_commit_message_selection(suggestions)
|
|
400
|
+
|
|
401
|
+
def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
|
|
402
|
+
self._display_commit_suggestions(suggestions)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
choice = self.console.input(
|
|
406
|
+
f"\nSelect message (1 - {len(suggestions)}) or enter custom: ",
|
|
407
|
+
)
|
|
408
|
+
return self._process_commit_choice(choice, suggestions)
|
|
409
|
+
except (KeyboardInterrupt, EOFError):
|
|
410
|
+
return suggestions[0]
|
|
411
|
+
|
|
412
|
+
def _display_commit_suggestions(self, suggestions: list[str]) -> None:
|
|
413
|
+
self.console.print("[cyan]📝[/cyan] Commit message suggestions: ")
|
|
414
|
+
for i, suggestion in enumerate(suggestions, 1):
|
|
415
|
+
self.console.print(f" {i}. {suggestion}")
|
|
416
|
+
|
|
417
|
+
def _process_commit_choice(self, choice: str, suggestions: list[str]) -> str:
|
|
418
|
+
if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
|
|
419
|
+
return suggestions[int(choice) - 1]
|
|
420
|
+
return choice or suggestions[0]
|
|
421
|
+
|
|
422
|
+
def _execute_hooks_with_retry(
|
|
423
|
+
self,
|
|
424
|
+
hook_type: str,
|
|
425
|
+
hook_runner: t.Callable[[], list[t.Any]],
|
|
426
|
+
options: OptionsProtocol,
|
|
427
|
+
) -> bool:
|
|
428
|
+
self._initialize_hook_execution(hook_type)
|
|
429
|
+
max_retries = self._get_max_retries(hook_type)
|
|
430
|
+
|
|
431
|
+
for attempt in range(max_retries):
|
|
432
|
+
try:
|
|
433
|
+
results = hook_runner()
|
|
434
|
+
summary = self.hook_manager.get_hook_summary(results)
|
|
435
|
+
|
|
436
|
+
if self._has_hook_failures(summary):
|
|
437
|
+
if self._should_retry_hooks(
|
|
438
|
+
hook_type,
|
|
439
|
+
attempt,
|
|
440
|
+
max_retries,
|
|
441
|
+
results,
|
|
442
|
+
):
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
return self._handle_hook_failures(
|
|
446
|
+
hook_type,
|
|
447
|
+
options,
|
|
448
|
+
summary,
|
|
449
|
+
results,
|
|
450
|
+
attempt,
|
|
451
|
+
max_retries,
|
|
452
|
+
)
|
|
453
|
+
return self._handle_hook_success(hook_type, summary)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
return self._handle_hook_exception(hook_type, e)
|
|
457
|
+
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
def _initialize_hook_execution(self, hook_type: str) -> None:
|
|
461
|
+
self.logger.info(f"Starting {hook_type} hooks execution")
|
|
462
|
+
self.session.track_task(
|
|
463
|
+
f"{hook_type}_hooks",
|
|
464
|
+
f"{hook_type.title()} hooks execution",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _get_max_retries(self, hook_type: str) -> int:
|
|
468
|
+
return 2 if hook_type == "fast" else 1
|
|
469
|
+
|
|
470
|
+
def _has_hook_failures(self, summary: dict[str, t.Any]) -> bool:
|
|
471
|
+
return summary["failed"] > 0 or summary["errors"] > 0
|
|
472
|
+
|
|
473
|
+
def _should_retry_hooks(
|
|
474
|
+
self,
|
|
475
|
+
hook_type: str,
|
|
476
|
+
attempt: int,
|
|
477
|
+
max_retries: int,
|
|
478
|
+
results: list[t.Any],
|
|
479
|
+
) -> bool:
|
|
480
|
+
if hook_type == "fast" and attempt < max_retries - 1:
|
|
481
|
+
if self._should_retry_fast_hooks(results):
|
|
482
|
+
self.console.print(
|
|
483
|
+
"[yellow]🔄[/yellow] Fast hooks modified files, retrying all fast hooks...",
|
|
484
|
+
)
|
|
485
|
+
return True
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
def _handle_hook_failures(
|
|
489
|
+
self,
|
|
490
|
+
hook_type: str,
|
|
491
|
+
options: OptionsProtocol,
|
|
492
|
+
summary: dict[str, t.Any],
|
|
493
|
+
results: list[t.Any],
|
|
494
|
+
attempt: int,
|
|
495
|
+
max_retries: int,
|
|
496
|
+
) -> bool:
|
|
497
|
+
self.logger.warning(
|
|
498
|
+
f"{hook_type} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
self.console.print(
|
|
502
|
+
f"[red]❌[/red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
503
|
+
)
|
|
504
|
+
self.session.fail_task(
|
|
505
|
+
f"{hook_type}_hooks",
|
|
506
|
+
f"{summary['failed']} failed, {summary['errors']} errors",
|
|
507
|
+
)
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def _should_retry_fast_hooks(self, results: list[t.Any]) -> bool:
|
|
511
|
+
formatting_hooks = {
|
|
512
|
+
"ruff-format",
|
|
513
|
+
"ruff-check",
|
|
514
|
+
"trailing-whitespace",
|
|
515
|
+
"end-of-file-fixer",
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for result in results:
|
|
519
|
+
hook_id = getattr(result, "hook_id", "") or getattr(result, "name", "")
|
|
520
|
+
if (
|
|
521
|
+
hook_id in formatting_hooks
|
|
522
|
+
and hasattr(result, "failed")
|
|
523
|
+
and result.failed
|
|
524
|
+
):
|
|
525
|
+
output = getattr(result, "output", "") or getattr(result, "stdout", "")
|
|
526
|
+
if any(
|
|
527
|
+
phrase in output.lower()
|
|
528
|
+
for phrase in (
|
|
529
|
+
"files were modified",
|
|
530
|
+
"fixed",
|
|
531
|
+
"reformatted",
|
|
532
|
+
"fixing",
|
|
533
|
+
)
|
|
534
|
+
):
|
|
535
|
+
return True
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
def _apply_retry_backoff(self, attempt: int) -> None:
|
|
539
|
+
if attempt > 0:
|
|
540
|
+
backoff_delay = 2 ** (attempt - 1)
|
|
541
|
+
self.logger.debug(f"Applying exponential backoff: {backoff_delay}s")
|
|
542
|
+
self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/dim]")
|
|
543
|
+
time.sleep(backoff_delay)
|
|
544
|
+
|
|
545
|
+
def _handle_hook_success(self, hook_type: str, summary: dict[str, t.Any]) -> bool:
|
|
546
|
+
self.logger.info(
|
|
547
|
+
f"{hook_type} hooks passed: {summary['passed']} / {summary['total']}",
|
|
548
|
+
)
|
|
549
|
+
self.console.print(
|
|
550
|
+
f"[green]✅[/green] {hook_type.title()} hooks passed: {summary['passed']} / {summary['total']}",
|
|
551
|
+
)
|
|
552
|
+
self.session.complete_task(
|
|
553
|
+
f"{hook_type}_hooks",
|
|
554
|
+
f"{summary['passed']} / {summary['total']} passed",
|
|
555
|
+
)
|
|
556
|
+
return True
|
|
557
|
+
|
|
558
|
+
def _handle_hook_exception(self, hook_type: str, e: Exception) -> bool:
|
|
559
|
+
self.console.print(f"[red]❌[/red] {hook_type.title()} hooks error: {e}")
|
|
560
|
+
self.session.fail_task(f"{hook_type}_hooks", str(e))
|
|
561
|
+
return False
|