crackerjack 0.31.10__py3-none-any.whl → 0.31.12__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 +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,8 +5,10 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from crackerjack.code_cleaner import CodeCleaner
|
|
8
|
+
from crackerjack.code_cleaner import CodeCleaner, PackageCleaningResult
|
|
9
|
+
from crackerjack.core.autofix_coordinator import AutofixCoordinator
|
|
9
10
|
from crackerjack.models.protocols import (
|
|
11
|
+
ConfigMergeServiceProtocol,
|
|
10
12
|
FileSystemInterface,
|
|
11
13
|
GitInterface,
|
|
12
14
|
HookManager,
|
|
@@ -30,6 +32,7 @@ class PhaseCoordinator:
|
|
|
30
32
|
hook_manager: HookManager,
|
|
31
33
|
test_manager: TestManagerProtocol,
|
|
32
34
|
publish_manager: PublishManager,
|
|
35
|
+
config_merge_service: ConfigMergeServiceProtocol,
|
|
33
36
|
) -> None:
|
|
34
37
|
self.console = console
|
|
35
38
|
self.pkg_path = pkg_path
|
|
@@ -40,9 +43,22 @@ class PhaseCoordinator:
|
|
|
40
43
|
self.hook_manager = hook_manager
|
|
41
44
|
self.test_manager = test_manager
|
|
42
45
|
self.publish_manager = publish_manager
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
self.config_merge_service = config_merge_service
|
|
47
|
+
|
|
48
|
+
self.code_cleaner = CodeCleaner(
|
|
49
|
+
console=console,
|
|
50
|
+
base_directory=pkg_path,
|
|
51
|
+
file_processor=None,
|
|
52
|
+
error_handler=None,
|
|
53
|
+
pipeline=None,
|
|
54
|
+
logger=None,
|
|
55
|
+
security_logger=None,
|
|
56
|
+
backup_service=None,
|
|
57
|
+
)
|
|
45
58
|
self.config_service = ConfigurationService(console=console, pkg_path=pkg_path)
|
|
59
|
+
self.autofix_coordinator = AutofixCoordinator(
|
|
60
|
+
console=console, pkg_path=pkg_path
|
|
61
|
+
)
|
|
46
62
|
|
|
47
63
|
self.logger = logging.getLogger("crackerjack.phases")
|
|
48
64
|
|
|
@@ -55,82 +71,226 @@ class PhaseCoordinator:
|
|
|
55
71
|
self._display_cleaning_header()
|
|
56
72
|
return self._execute_cleaning_process()
|
|
57
73
|
except Exception as e:
|
|
58
|
-
self.console.print(f"[red]❌[/red] Cleaning failed: {e}")
|
|
74
|
+
self.console.print(f"[red]❌[/ red] Cleaning failed: {e}")
|
|
59
75
|
self.session.fail_task("cleaning", str(e))
|
|
60
76
|
return False
|
|
61
77
|
|
|
62
78
|
def _display_cleaning_header(self) -> None:
|
|
63
|
-
self.console.print("\n" + "
|
|
79
|
+
self.console.print("\n" + "-" * 80)
|
|
64
80
|
self.console.print(
|
|
65
81
|
"[bold bright_magenta]🛠️ SETUP[/bold bright_magenta] [bold bright_white]Initializing project structure[/bold bright_white]",
|
|
66
82
|
)
|
|
67
|
-
self.console.print("
|
|
83
|
+
self.console.print("-" * 80 + "\n")
|
|
68
84
|
self.console.print("[yellow]🧹[/yellow] Starting code cleaning...")
|
|
69
85
|
|
|
70
86
|
def _execute_cleaning_process(self) -> bool:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
# Use the comprehensive backup cleaning system for safety
|
|
88
|
+
cleaning_result = self.code_cleaner.clean_files(self.pkg_path, use_backup=True)
|
|
89
|
+
|
|
90
|
+
if isinstance(cleaning_result, list):
|
|
91
|
+
# Legacy mode (should not happen with use_backup=True, but handle gracefully)
|
|
92
|
+
cleaned_files = [str(r.file_path) for r in cleaning_result if r.success]
|
|
93
|
+
self._report_cleaning_results(cleaned_files)
|
|
94
|
+
return all(r.success for r in cleaning_result) if cleaning_result else True
|
|
95
|
+
else:
|
|
96
|
+
# PackageCleaningResult from backup mode
|
|
97
|
+
self._report_package_cleaning_results(cleaning_result)
|
|
98
|
+
return cleaning_result.overall_success
|
|
79
99
|
|
|
80
100
|
def _handle_no_files_to_clean(self) -> bool:
|
|
81
|
-
self.console.print("[yellow]⚠️[/yellow] No Python files found to clean")
|
|
101
|
+
self.console.print("[yellow]⚠️[/ yellow] No Python files found to clean")
|
|
82
102
|
self.session.complete_task("cleaning", "No files to clean")
|
|
83
103
|
return True
|
|
84
104
|
|
|
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
105
|
def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
|
|
94
106
|
if cleaned_files:
|
|
95
|
-
self.console.print(f"[green]✅[/green] Cleaned {len(cleaned_files)} files")
|
|
107
|
+
self.console.print(f"[green]✅[/ green] Cleaned {len(cleaned_files)} files")
|
|
96
108
|
self.session.complete_task(
|
|
97
109
|
"cleaning",
|
|
98
110
|
f"Cleaned {len(cleaned_files)} files",
|
|
99
111
|
)
|
|
100
112
|
else:
|
|
101
|
-
self.console.print("[green]✅[/green] No cleaning needed")
|
|
113
|
+
self.console.print("[green]✅[/ green] No cleaning needed")
|
|
102
114
|
self.session.complete_task("cleaning", "No cleaning needed")
|
|
103
115
|
|
|
116
|
+
def _report_package_cleaning_results(self, result: PackageCleaningResult) -> None:
|
|
117
|
+
"""Report package cleaning results with backup information."""
|
|
118
|
+
if result.overall_success:
|
|
119
|
+
self.console.print(
|
|
120
|
+
f"[green]✅[/ green] Package cleaning completed successfully! "
|
|
121
|
+
f"({result.successful_files}/{result.total_files} files cleaned)"
|
|
122
|
+
)
|
|
123
|
+
self.session.complete_task(
|
|
124
|
+
"cleaning",
|
|
125
|
+
f"Cleaned {result.successful_files}/{result.total_files} files with backup protection",
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
self.console.print(
|
|
129
|
+
f"[red]❌[/ red] Package cleaning failed! "
|
|
130
|
+
f"({result.failed_files}/{result.total_files} files failed)"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result.backup_restored:
|
|
134
|
+
self.console.print(
|
|
135
|
+
"[yellow]⚠️[/ yellow] Files were automatically restored from backup"
|
|
136
|
+
)
|
|
137
|
+
self.session.complete_task(
|
|
138
|
+
"cleaning", "Failed with automatic backup restoration"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
self.session.fail_task(
|
|
142
|
+
"cleaning", f"Failed to clean {result.failed_files} files"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if result.backup_metadata:
|
|
146
|
+
self.console.print(
|
|
147
|
+
f"[blue]📦[/ blue] Backup available at: {result.backup_metadata.backup_directory}"
|
|
148
|
+
)
|
|
149
|
+
|
|
104
150
|
def run_configuration_phase(self, options: OptionsProtocol) -> bool:
|
|
105
151
|
if options.no_config_updates:
|
|
106
152
|
return True
|
|
107
153
|
self.session.track_task("configuration", "Configuration updates")
|
|
108
154
|
try:
|
|
109
|
-
success =
|
|
155
|
+
success = self._execute_configuration_steps(options)
|
|
156
|
+
self._complete_configuration_task(success)
|
|
157
|
+
return success
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.session.fail_task("configuration", str(e))
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _execute_configuration_steps(self, options: OptionsProtocol) -> bool:
|
|
163
|
+
"""Execute all configuration steps and return overall success."""
|
|
164
|
+
success = True
|
|
165
|
+
|
|
166
|
+
# FIRST STEP: Smart config merge before all other operations
|
|
167
|
+
self._handle_smart_config_merge(options)
|
|
168
|
+
|
|
169
|
+
# Handle crackerjack project specific configuration
|
|
170
|
+
if self._is_crackerjack_project() and not self._copy_config_files_to_package():
|
|
171
|
+
success = False
|
|
172
|
+
|
|
173
|
+
# Update configuration files
|
|
174
|
+
success &= self._update_configuration_files(options)
|
|
175
|
+
|
|
176
|
+
return success
|
|
177
|
+
|
|
178
|
+
def _handle_smart_config_merge(self, options: OptionsProtocol) -> None:
|
|
179
|
+
"""Handle smart config merge with warning on failure."""
|
|
180
|
+
if not self._perform_smart_config_merge(options):
|
|
181
|
+
self.console.print(
|
|
182
|
+
"[yellow]⚠️[/yellow] Smart config merge encountered issues (continuing)"
|
|
183
|
+
)
|
|
184
|
+
# Don't fail the entire configuration phase, just log the warning
|
|
185
|
+
|
|
186
|
+
def _update_configuration_files(self, options: OptionsProtocol) -> bool:
|
|
187
|
+
"""Update precommit and pyproject configuration files."""
|
|
188
|
+
success = True
|
|
189
|
+
if not self.config_service.update_precommit_config(options):
|
|
190
|
+
success = False
|
|
191
|
+
if not self.config_service.update_pyproject_config(options):
|
|
192
|
+
success = False
|
|
193
|
+
return success
|
|
194
|
+
|
|
195
|
+
def _complete_configuration_task(self, success: bool) -> None:
|
|
196
|
+
"""Complete the configuration task with appropriate message."""
|
|
197
|
+
message = (
|
|
198
|
+
"Configuration updated successfully"
|
|
199
|
+
if success
|
|
200
|
+
else "Some configuration updates failed"
|
|
201
|
+
)
|
|
202
|
+
self.session.complete_task("configuration", message)
|
|
203
|
+
|
|
204
|
+
def _perform_smart_config_merge(self, options: OptionsProtocol) -> bool:
|
|
205
|
+
"""Perform smart config merge before git operations."""
|
|
206
|
+
try:
|
|
207
|
+
self.logger.debug("Starting smart config merge process")
|
|
110
208
|
|
|
111
|
-
#
|
|
209
|
+
# Smart merge for critical configuration files
|
|
210
|
+
merged_files = []
|
|
211
|
+
|
|
212
|
+
# Skip smart merge if explicitly requested or in specific modes
|
|
213
|
+
if hasattr(options, "skip_config_merge") and options.skip_config_merge:
|
|
214
|
+
self.logger.debug("Config merge skipped by option")
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
# Merge .gitignore patterns (always safe to do)
|
|
218
|
+
if self._smart_merge_gitignore():
|
|
219
|
+
merged_files.append(".gitignore")
|
|
220
|
+
|
|
221
|
+
# Merge configuration files (pyproject.toml, .pre-commit-config.yaml)
|
|
222
|
+
# Only for crackerjack projects to avoid breaking user projects
|
|
112
223
|
if self._is_crackerjack_project():
|
|
113
|
-
if
|
|
114
|
-
|
|
224
|
+
if self._smart_merge_project_configs():
|
|
225
|
+
merged_files.extend(["pyproject.toml", ".pre-commit-config.yaml"])
|
|
115
226
|
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
227
|
+
if merged_files:
|
|
228
|
+
files_str = ", ".join(merged_files)
|
|
229
|
+
self.console.print(
|
|
230
|
+
f"[green]🔧[/green] Smart-merged configurations: {files_str}"
|
|
231
|
+
)
|
|
232
|
+
self.logger.info(f"Smart config merge completed: {merged_files}")
|
|
233
|
+
else:
|
|
234
|
+
self.logger.debug("No configuration files needed smart merging")
|
|
235
|
+
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.console.print(f"[yellow]⚠️[/yellow] Smart config merge failed: {e}")
|
|
240
|
+
self.logger.warning(
|
|
241
|
+
f"Smart config merge failed: {e} (type: {type(e).__name__})"
|
|
125
242
|
)
|
|
126
|
-
|
|
243
|
+
# Return True to not block the workflow - this is fail-safe
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
def _smart_merge_gitignore(self) -> bool:
|
|
247
|
+
"""Smart merge .gitignore patterns."""
|
|
248
|
+
try:
|
|
249
|
+
gitignore_path = self.pkg_path / ".gitignore"
|
|
250
|
+
if not gitignore_path.exists():
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
# Standard crackerjack ignore patterns to merge
|
|
254
|
+
standard_patterns = [
|
|
255
|
+
"# Crackerjack generated files",
|
|
256
|
+
".crackerjack/",
|
|
257
|
+
"*.crackerjack.bak",
|
|
258
|
+
".coverage.*",
|
|
259
|
+
"crackerjack-debug-*.log",
|
|
260
|
+
"__pycache__/",
|
|
261
|
+
"*.py[cod]",
|
|
262
|
+
"*$py.class",
|
|
263
|
+
".pytest_cache/",
|
|
264
|
+
".tox/",
|
|
265
|
+
".mypy_cache/",
|
|
266
|
+
".ruff_cache/",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
self.config_merge_service.smart_merge_gitignore(
|
|
270
|
+
patterns=standard_patterns, target_path=str(gitignore_path)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return True
|
|
274
|
+
|
|
127
275
|
except Exception as e:
|
|
128
|
-
self.
|
|
276
|
+
self.logger.warning(f"Failed to smart merge .gitignore: {e}")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
def _smart_merge_project_configs(self) -> bool:
|
|
280
|
+
"""Smart merge pyproject.toml and pre-commit config for crackerjack projects."""
|
|
281
|
+
try:
|
|
282
|
+
# This would be where we implement project config merging
|
|
283
|
+
# For now, just return True as the existing config service handles this
|
|
284
|
+
self.logger.debug(
|
|
285
|
+
"Project config smart merge placeholder - handled by existing config service"
|
|
286
|
+
)
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.logger.warning(f"Failed to smart merge project configs: {e}")
|
|
129
291
|
return False
|
|
130
292
|
|
|
131
293
|
def _is_crackerjack_project(self) -> bool:
|
|
132
|
-
"""Check if we're running from the crackerjack project root."""
|
|
133
|
-
# Check for crackerjack-specific markers
|
|
134
294
|
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
135
295
|
if not pyproject_path.exists():
|
|
136
296
|
return False
|
|
@@ -141,16 +301,13 @@ class PhaseCoordinator:
|
|
|
141
301
|
with pyproject_path.open("rb") as f:
|
|
142
302
|
data = tomllib.load(f)
|
|
143
303
|
|
|
144
|
-
# Check if this is the crackerjack project
|
|
145
304
|
project_name = data.get("project", {}).get("name", "")
|
|
146
305
|
return project_name == "crackerjack"
|
|
147
306
|
except Exception:
|
|
148
307
|
return False
|
|
149
308
|
|
|
150
309
|
def _copy_config_files_to_package(self) -> bool:
|
|
151
|
-
"""Copy configuration files from project root to package root."""
|
|
152
310
|
try:
|
|
153
|
-
# Files to copy from project root to package root
|
|
154
311
|
files_to_copy = [
|
|
155
312
|
"pyproject.toml",
|
|
156
313
|
".pre-commit-config.yaml",
|
|
@@ -164,7 +321,7 @@ class PhaseCoordinator:
|
|
|
164
321
|
package_dir = self.pkg_path / "crackerjack"
|
|
165
322
|
if not package_dir.exists():
|
|
166
323
|
self.console.print(
|
|
167
|
-
"[yellow]⚠️[/yellow] Package directory not found: crackerjack/",
|
|
324
|
+
"[yellow]⚠️[/ yellow] Package directory not found: crackerjack /",
|
|
168
325
|
)
|
|
169
326
|
return False
|
|
170
327
|
|
|
@@ -181,18 +338,18 @@ class PhaseCoordinator:
|
|
|
181
338
|
self.logger.debug(f"Copied {filename} to package directory")
|
|
182
339
|
except Exception as e:
|
|
183
340
|
self.console.print(
|
|
184
|
-
f"[yellow]⚠️[/yellow] Failed to copy {filename}: {e}",
|
|
341
|
+
f"[yellow]⚠️[/ yellow] Failed to copy {filename}: {e}",
|
|
185
342
|
)
|
|
186
343
|
|
|
187
344
|
if copied_count > 0:
|
|
188
345
|
self.console.print(
|
|
189
|
-
f"[green]✅[/green] Copied {copied_count} config files to package directory",
|
|
346
|
+
f"[green]✅[/ green] Copied {copied_count} config files to package directory",
|
|
190
347
|
)
|
|
191
348
|
|
|
192
349
|
return True
|
|
193
350
|
except Exception as e:
|
|
194
351
|
self.console.print(
|
|
195
|
-
f"[red]❌[/red] Failed to copy config files to package: {e}",
|
|
352
|
+
f"[red]❌[/ red] Failed to copy config files to package: {e}",
|
|
196
353
|
)
|
|
197
354
|
return False
|
|
198
355
|
|
|
@@ -236,7 +393,7 @@ class PhaseCoordinator:
|
|
|
236
393
|
try:
|
|
237
394
|
self.console.print("\n" + "-" * 80)
|
|
238
395
|
self.console.print(
|
|
239
|
-
"[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
|
|
396
|
+
"[bold bright_blue]🧪 TESTS[/ bold bright_blue] [bold bright_white]Running test suite[/ bold bright_white]",
|
|
240
397
|
)
|
|
241
398
|
self.console.print("-" * 80 + "\n")
|
|
242
399
|
if not self.test_manager.validate_test_environment():
|
|
@@ -247,7 +404,7 @@ class PhaseCoordinator:
|
|
|
247
404
|
coverage_info = self.test_manager.get_coverage()
|
|
248
405
|
self.session.complete_task(
|
|
249
406
|
"testing",
|
|
250
|
-
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0)
|
|
407
|
+
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0): .1f}%",
|
|
251
408
|
)
|
|
252
409
|
else:
|
|
253
410
|
self.session.fail_task("testing", "Tests failed")
|
|
@@ -267,7 +424,7 @@ class PhaseCoordinator:
|
|
|
267
424
|
try:
|
|
268
425
|
return self._execute_publishing_workflow(options, version_type)
|
|
269
426
|
except Exception as e:
|
|
270
|
-
self.console.print(f"[red]❌[/red] Publishing failed: {e}")
|
|
427
|
+
self.console.print(f"[red]❌[/ red] Publishing failed: {e}")
|
|
271
428
|
self.session.fail_task("publishing", str(e))
|
|
272
429
|
return False
|
|
273
430
|
|
|
@@ -302,7 +459,7 @@ class PhaseCoordinator:
|
|
|
302
459
|
options: OptionsProtocol,
|
|
303
460
|
new_version: str,
|
|
304
461
|
) -> None:
|
|
305
|
-
self.console.print(f"[green]🚀[/green] Successfully published {new_version}!")
|
|
462
|
+
self.console.print(f"[green]🚀[/ green] Successfully published {new_version}!")
|
|
306
463
|
|
|
307
464
|
if options.cleanup_pypi:
|
|
308
465
|
self.publish_manager.cleanup_old_releases(options.keep_releases)
|
|
@@ -320,12 +477,12 @@ class PhaseCoordinator:
|
|
|
320
477
|
commit_message = self._get_commit_message(changed_files, options)
|
|
321
478
|
return self._execute_commit_and_push(changed_files, commit_message)
|
|
322
479
|
except Exception as e:
|
|
323
|
-
self.console.print(f"[red]❌[/red] Commit failed: {e}")
|
|
480
|
+
self.console.print(f"[red]❌[/ red] Commit failed: {e}")
|
|
324
481
|
self.session.fail_task("commit", str(e))
|
|
325
482
|
return False
|
|
326
483
|
|
|
327
484
|
def _handle_no_changes_to_commit(self) -> bool:
|
|
328
|
-
self.console.print("[yellow]ℹ️[/yellow] No changes to commit")
|
|
485
|
+
self.console.print("[yellow]ℹ️[/ yellow] No changes to commit")
|
|
329
486
|
self.session.complete_task("commit", "No changes to commit")
|
|
330
487
|
return True
|
|
331
488
|
|
|
@@ -347,7 +504,7 @@ class PhaseCoordinator:
|
|
|
347
504
|
def _handle_push_result(self, commit_message: str) -> bool:
|
|
348
505
|
if self.git_service.push():
|
|
349
506
|
self.console.print(
|
|
350
|
-
f"[green]🎉[/green] Committed and pushed: {commit_message}",
|
|
507
|
+
f"[green]🎉[/ green] Committed and pushed: {commit_message}",
|
|
351
508
|
)
|
|
352
509
|
self.session.complete_task(
|
|
353
510
|
"commit",
|
|
@@ -355,7 +512,7 @@ class PhaseCoordinator:
|
|
|
355
512
|
)
|
|
356
513
|
else:
|
|
357
514
|
self.console.print(
|
|
358
|
-
f"[yellow]⚠️[/yellow] Committed but push failed: {commit_message}",
|
|
515
|
+
f"[yellow]⚠️[/ yellow] Committed but push failed: {commit_message}",
|
|
359
516
|
)
|
|
360
517
|
self.session.complete_task(
|
|
361
518
|
"commit",
|
|
@@ -375,11 +532,11 @@ class PhaseCoordinator:
|
|
|
375
532
|
self.session.track_task("version_bump", f"Version bump ({bump_type})")
|
|
376
533
|
try:
|
|
377
534
|
new_version = self.publish_manager.bump_version(bump_type)
|
|
378
|
-
self.console.print(f"[green]🎯[/green] Version bumped to {new_version}")
|
|
535
|
+
self.console.print(f"[green]🎯[/ green] Version bumped to {new_version}")
|
|
379
536
|
self.session.complete_task("version_bump", f"Bumped to {new_version}")
|
|
380
537
|
return True
|
|
381
538
|
except Exception as e:
|
|
382
|
-
self.console.print(f"[red]❌[/red] Version bump failed: {e}")
|
|
539
|
+
self.console.print(f"[red]❌[/ red] Version bump failed: {e}")
|
|
383
540
|
self.session.fail_task("version_bump", str(e))
|
|
384
541
|
return False
|
|
385
542
|
|
|
@@ -403,14 +560,14 @@ class PhaseCoordinator:
|
|
|
403
560
|
|
|
404
561
|
try:
|
|
405
562
|
choice = self.console.input(
|
|
406
|
-
f"\nSelect message (1
|
|
563
|
+
f"\nSelect message (1-{len(suggestions)}) or enter custom: ",
|
|
407
564
|
)
|
|
408
565
|
return self._process_commit_choice(choice, suggestions)
|
|
409
566
|
except (KeyboardInterrupt, EOFError):
|
|
410
567
|
return suggestions[0]
|
|
411
568
|
|
|
412
569
|
def _display_commit_suggestions(self, suggestions: list[str]) -> None:
|
|
413
|
-
self.console.print("[cyan]📝[/cyan] Commit message suggestions: ")
|
|
570
|
+
self.console.print("[cyan]📝[/ cyan] Commit message suggestions: ")
|
|
414
571
|
for i, suggestion in enumerate(suggestions, 1):
|
|
415
572
|
self.console.print(f" {i}. {suggestion}")
|
|
416
573
|
|
|
@@ -480,11 +637,21 @@ class PhaseCoordinator:
|
|
|
480
637
|
if hook_type == "fast" and attempt < max_retries - 1:
|
|
481
638
|
if self._should_retry_fast_hooks(results):
|
|
482
639
|
self.console.print(
|
|
483
|
-
"[yellow]🔄[/yellow] Fast hooks modified files, retrying all fast hooks...",
|
|
640
|
+
"[yellow]🔄[/ yellow] Fast hooks modified files, retrying all fast hooks...",
|
|
484
641
|
)
|
|
485
642
|
return True
|
|
486
643
|
return False
|
|
487
644
|
|
|
645
|
+
def _attempt_autofix_for_fast_hooks(self, results: list[t.Any]) -> bool:
|
|
646
|
+
"""Attempt to autofix fast hook failures."""
|
|
647
|
+
try:
|
|
648
|
+
self.logger.info("Attempting autofix for fast hook failures")
|
|
649
|
+
# Apply autofixes for fast hooks
|
|
650
|
+
return self.autofix_coordinator.apply_fast_stage_fixes()
|
|
651
|
+
except Exception as e:
|
|
652
|
+
self.logger.warning(f"Autofix attempt failed: {e}")
|
|
653
|
+
return False
|
|
654
|
+
|
|
488
655
|
def _handle_hook_failures(
|
|
489
656
|
self,
|
|
490
657
|
hook_type: str,
|
|
@@ -494,15 +661,26 @@ class PhaseCoordinator:
|
|
|
494
661
|
attempt: int,
|
|
495
662
|
max_retries: int,
|
|
496
663
|
) -> bool:
|
|
497
|
-
self.logger.
|
|
664
|
+
self.logger.debug(
|
|
498
665
|
f"{hook_type} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
499
666
|
)
|
|
500
667
|
|
|
501
668
|
self.console.print(
|
|
502
|
-
f"[red]❌[/red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
669
|
+
f"[red]❌[/ red] {hook_type.title()} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
503
670
|
)
|
|
504
671
|
|
|
505
|
-
#
|
|
672
|
+
# Try autofix for fast hooks before giving up
|
|
673
|
+
if hook_type == "fast" and attempt < max_retries - 1:
|
|
674
|
+
if self._attempt_autofix_for_fast_hooks(results):
|
|
675
|
+
self.console.print(
|
|
676
|
+
"[yellow]🔧[/ yellow] Applied autofixes for fast hooks, retrying...",
|
|
677
|
+
)
|
|
678
|
+
return True # Return True to continue the retry loop
|
|
679
|
+
|
|
680
|
+
# Display detailed hook errors in verbose mode
|
|
681
|
+
if getattr(options, "verbose", False):
|
|
682
|
+
self._display_verbose_hook_errors(results, hook_type)
|
|
683
|
+
|
|
506
684
|
detailed_error_msg = self._build_detailed_hook_error_message(results, summary)
|
|
507
685
|
|
|
508
686
|
self.session.fail_task(
|
|
@@ -511,13 +689,40 @@ class PhaseCoordinator:
|
|
|
511
689
|
)
|
|
512
690
|
return False
|
|
513
691
|
|
|
692
|
+
def _display_verbose_hook_errors(
|
|
693
|
+
self, results: list[t.Any], hook_type: str
|
|
694
|
+
) -> None:
|
|
695
|
+
"""Display detailed hook error output in verbose mode."""
|
|
696
|
+
self.console.print(
|
|
697
|
+
f"\n[bold yellow]📋 Detailed {hook_type} hook errors:[/bold yellow]"
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
for result in results:
|
|
701
|
+
# Check if this hook failed
|
|
702
|
+
status = getattr(result, "status", "")
|
|
703
|
+
if status not in ("failed", "error", "timeout"):
|
|
704
|
+
continue
|
|
705
|
+
|
|
706
|
+
hook_name = getattr(result, "name", "unknown")
|
|
707
|
+
issues = getattr(result, "issues_found", [])
|
|
708
|
+
|
|
709
|
+
self.console.print(f"\n[red]❌ {hook_name}[/red]")
|
|
710
|
+
|
|
711
|
+
if issues:
|
|
712
|
+
for issue in issues:
|
|
713
|
+
if isinstance(issue, str) and issue.strip():
|
|
714
|
+
# Clean up the issue text and display with proper indentation
|
|
715
|
+
cleaned_issue = issue.strip()
|
|
716
|
+
self.console.print(f" {cleaned_issue}")
|
|
717
|
+
else:
|
|
718
|
+
# If no specific issues, show generic failure message
|
|
719
|
+
self.console.print(f" Hook failed with exit code (status: {status})")
|
|
720
|
+
|
|
514
721
|
def _build_detailed_hook_error_message(
|
|
515
722
|
self, results: list[t.Any], summary: dict[str, t.Any]
|
|
516
723
|
) -> str:
|
|
517
|
-
"""Build detailed error message with specific hook failure information."""
|
|
518
724
|
error_parts = [f"{summary['failed']} failed, {summary['errors']} errors"]
|
|
519
725
|
|
|
520
|
-
# Extract specific hook failures
|
|
521
726
|
failed_hooks = []
|
|
522
727
|
for result in results:
|
|
523
728
|
if hasattr(result, "failed") and result.failed:
|
|
@@ -532,38 +737,20 @@ class PhaseCoordinator:
|
|
|
532
737
|
return " | ".join(error_parts)
|
|
533
738
|
|
|
534
739
|
def _should_retry_fast_hooks(self, results: list[t.Any]) -> bool:
|
|
535
|
-
formatting_hooks = {
|
|
536
|
-
"ruff-format",
|
|
537
|
-
"ruff-check",
|
|
538
|
-
"trailing-whitespace",
|
|
539
|
-
"end-of-file-fixer",
|
|
540
|
-
}
|
|
541
|
-
|
|
542
740
|
for result in results:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
output = getattr(result, "output", "") or getattr(result, "stdout", "")
|
|
550
|
-
if any(
|
|
551
|
-
phrase in output.lower()
|
|
552
|
-
for phrase in (
|
|
553
|
-
"files were modified",
|
|
554
|
-
"fixed",
|
|
555
|
-
"reformatted",
|
|
556
|
-
"fixing",
|
|
557
|
-
)
|
|
558
|
-
):
|
|
559
|
-
return True
|
|
741
|
+
if hasattr(result, "failed") and result.failed:
|
|
742
|
+
return True
|
|
743
|
+
|
|
744
|
+
status = getattr(result, "status", "")
|
|
745
|
+
if status in ("failed", "error", "timeout"):
|
|
746
|
+
return True
|
|
560
747
|
return False
|
|
561
748
|
|
|
562
749
|
def _apply_retry_backoff(self, attempt: int) -> None:
|
|
563
750
|
if attempt > 0:
|
|
564
751
|
backoff_delay = 2 ** (attempt - 1)
|
|
565
752
|
self.logger.debug(f"Applying exponential backoff: {backoff_delay}s")
|
|
566
|
-
self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/dim]")
|
|
753
|
+
self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/ dim]")
|
|
567
754
|
time.sleep(backoff_delay)
|
|
568
755
|
|
|
569
756
|
def _handle_hook_success(self, hook_type: str, summary: dict[str, t.Any]) -> bool:
|
|
@@ -571,7 +758,7 @@ class PhaseCoordinator:
|
|
|
571
758
|
f"{hook_type} hooks passed: {summary['passed']} / {summary['total']}",
|
|
572
759
|
)
|
|
573
760
|
self.console.print(
|
|
574
|
-
f"[green]✅[/green] {hook_type.title()} hooks passed: {summary['passed']} / {summary['total']}",
|
|
761
|
+
f"[green]✅[/ green] {hook_type.title()} hooks passed: {summary['passed']} / {summary['total']}",
|
|
575
762
|
)
|
|
576
763
|
self.session.complete_task(
|
|
577
764
|
f"{hook_type}_hooks",
|
|
@@ -580,6 +767,6 @@ class PhaseCoordinator:
|
|
|
580
767
|
return True
|
|
581
768
|
|
|
582
769
|
def _handle_hook_exception(self, hook_type: str, e: Exception) -> bool:
|
|
583
|
-
self.console.print(f"[red]❌[/red] {hook_type.title()} hooks error: {e}")
|
|
770
|
+
self.console.print(f"[red]❌[/ red] {hook_type.title()} hooks error: {e}")
|
|
584
771
|
self.session.fail_task(f"{hook_type}_hooks", str(e))
|
|
585
772
|
return False
|