crackerjack 0.30.3__py3-none-any.whl â 0.31.7__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 +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -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 +657 -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 +409 -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 +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -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 +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -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 +372 -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 +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -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/coverage_improvement.py +223 -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 +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -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 +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -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.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.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.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info â crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
crackerjack/api.py
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing as t
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from .code_cleaner import CleaningResult, CodeCleaner
|
|
9
|
+
from .core.workflow_orchestrator import WorkflowOrchestrator
|
|
10
|
+
from .errors import CrackerjackError, ErrorCode
|
|
11
|
+
from .interactive import InteractiveCLI
|
|
12
|
+
from .interactive import WorkflowOptions as InteractiveWorkflowOptions
|
|
13
|
+
from .models.config import WorkflowOptions
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class QualityCheckResult:
|
|
18
|
+
success: bool
|
|
19
|
+
fast_hooks_passed: bool
|
|
20
|
+
comprehensive_hooks_passed: bool
|
|
21
|
+
errors: list[str]
|
|
22
|
+
warnings: list[str]
|
|
23
|
+
duration: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TestResult:
|
|
28
|
+
success: bool
|
|
29
|
+
passed_count: int
|
|
30
|
+
failed_count: int
|
|
31
|
+
coverage_percentage: float
|
|
32
|
+
duration: float
|
|
33
|
+
errors: list[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class PublishResult:
|
|
38
|
+
success: bool
|
|
39
|
+
version: str
|
|
40
|
+
published_to: list[str]
|
|
41
|
+
errors: list[str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CrackerjackAPI:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
project_path: Path | None = None,
|
|
48
|
+
console: Console | None = None,
|
|
49
|
+
verbose: bool = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.project_path = project_path or Path.cwd()
|
|
52
|
+
self.console = console or Console()
|
|
53
|
+
self.verbose = verbose
|
|
54
|
+
|
|
55
|
+
self.orchestrator = WorkflowOrchestrator(
|
|
56
|
+
console=self.console,
|
|
57
|
+
pkg_path=self.project_path,
|
|
58
|
+
verbose=self.verbose,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.container = self.orchestrator.container
|
|
62
|
+
|
|
63
|
+
self._code_cleaner: CodeCleaner | None = None
|
|
64
|
+
self._interactive_cli: InteractiveCLI | None = None
|
|
65
|
+
|
|
66
|
+
import logging
|
|
67
|
+
|
|
68
|
+
self.logger = logging.getLogger("crackerjack.api")
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def code_cleaner(self) -> CodeCleaner:
|
|
72
|
+
if self._code_cleaner is None:
|
|
73
|
+
self._code_cleaner = CodeCleaner(console=self.console)
|
|
74
|
+
return self._code_cleaner
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def interactive_cli(self) -> InteractiveCLI:
|
|
78
|
+
if self._interactive_cli is None:
|
|
79
|
+
self._interactive_cli = InteractiveCLI(console=self.console)
|
|
80
|
+
return self._interactive_cli
|
|
81
|
+
|
|
82
|
+
def run_quality_checks(
|
|
83
|
+
self,
|
|
84
|
+
fast_only: bool = False,
|
|
85
|
+
autofix: bool = True,
|
|
86
|
+
) -> QualityCheckResult:
|
|
87
|
+
import asyncio
|
|
88
|
+
import time
|
|
89
|
+
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
self.logger.info("Starting quality checks")
|
|
94
|
+
|
|
95
|
+
options = self._create_options(autofix=autofix, skip_hooks=False)
|
|
96
|
+
|
|
97
|
+
success = asyncio.run(
|
|
98
|
+
self.orchestrator.pipeline.run_complete_workflow(options),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
duration = time.time() - start_time
|
|
102
|
+
|
|
103
|
+
return QualityCheckResult(
|
|
104
|
+
success=success,
|
|
105
|
+
fast_hooks_passed=success,
|
|
106
|
+
comprehensive_hooks_passed=success if not fast_only else True,
|
|
107
|
+
errors=[] if success else ["Quality checks failed"],
|
|
108
|
+
warnings=[],
|
|
109
|
+
duration=duration,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
duration = time.time() - start_time
|
|
114
|
+
self.logger.exception(f"Quality checks failed: {e}")
|
|
115
|
+
|
|
116
|
+
return QualityCheckResult(
|
|
117
|
+
success=False,
|
|
118
|
+
fast_hooks_passed=False,
|
|
119
|
+
comprehensive_hooks_passed=False,
|
|
120
|
+
errors=[str(e)],
|
|
121
|
+
warnings=[],
|
|
122
|
+
duration=duration,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def clean_code(
|
|
126
|
+
self,
|
|
127
|
+
target_dir: Path | None = None,
|
|
128
|
+
backup: bool = True,
|
|
129
|
+
) -> list[CleaningResult]:
|
|
130
|
+
"""Clean code with TODO validation and comprehensive error handling."""
|
|
131
|
+
target_dir = target_dir or self.project_path
|
|
132
|
+
self.logger.info(f"Cleaning code in {target_dir}")
|
|
133
|
+
|
|
134
|
+
self._validate_code_before_cleaning(target_dir)
|
|
135
|
+
self._notify_backup_status(backup)
|
|
136
|
+
|
|
137
|
+
return self._execute_code_cleaning(target_dir)
|
|
138
|
+
|
|
139
|
+
def _validate_code_before_cleaning(self, target_dir: Path) -> None:
|
|
140
|
+
"""Validate code state before cleaning, checking for TODOs."""
|
|
141
|
+
todos_found = self._check_for_todos(target_dir)
|
|
142
|
+
if todos_found:
|
|
143
|
+
self._handle_todos_found(todos_found, target_dir)
|
|
144
|
+
|
|
145
|
+
def _handle_todos_found(
|
|
146
|
+
self,
|
|
147
|
+
todos_found: list[tuple[Path, int, str]],
|
|
148
|
+
target_dir: Path,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Handle case where TODOs are found in codebase."""
|
|
151
|
+
todo_count = len(todos_found)
|
|
152
|
+
self.console.print(f"[red]â Found {todo_count} TODO(s) in codebase[/red]")
|
|
153
|
+
self.console.print(
|
|
154
|
+
"[yellow]Please resolve all TODOs before running code cleaning ( - x)[/yellow]",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self._display_todo_summary(todos_found, target_dir, todo_count)
|
|
158
|
+
|
|
159
|
+
raise CrackerjackError(
|
|
160
|
+
message=f"Found {todo_count} TODO(s) in codebase. Resolve them before cleaning.",
|
|
161
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _display_todo_summary(
|
|
165
|
+
self,
|
|
166
|
+
todos_found: list[tuple[Path, int, str]],
|
|
167
|
+
target_dir: Path,
|
|
168
|
+
todo_count: int,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Display summary of found TODOs."""
|
|
171
|
+
for _i, (file_path, line_no, content) in enumerate(todos_found[:5]):
|
|
172
|
+
relative_path = file_path.relative_to(target_dir)
|
|
173
|
+
self.console.print(f" {relative_path}: {line_no}: {content.strip()}")
|
|
174
|
+
|
|
175
|
+
if todo_count > 5:
|
|
176
|
+
self.console.print(f" ... and {todo_count - 5} more")
|
|
177
|
+
|
|
178
|
+
def _notify_backup_status(self, backup: bool) -> None:
|
|
179
|
+
"""Notify user about backup file creation."""
|
|
180
|
+
if backup:
|
|
181
|
+
self.console.print("[yellow]Note: Backup files will be created[/yellow]")
|
|
182
|
+
|
|
183
|
+
def _execute_code_cleaning(self, target_dir: Path) -> list[CleaningResult]:
|
|
184
|
+
"""Execute code cleaning and handle results."""
|
|
185
|
+
try:
|
|
186
|
+
results = self.code_cleaner.clean_files(target_dir)
|
|
187
|
+
self._report_cleaning_results(results)
|
|
188
|
+
return results
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self._handle_cleaning_error(e)
|
|
191
|
+
|
|
192
|
+
def _report_cleaning_results(self, results: list[CleaningResult]) -> None:
|
|
193
|
+
"""Report cleaning results to user."""
|
|
194
|
+
successful = sum(1 for r in results if r.success)
|
|
195
|
+
failed = len(results) - successful
|
|
196
|
+
|
|
197
|
+
if successful > 0:
|
|
198
|
+
self.console.print(
|
|
199
|
+
f"[green]â
Successfully cleaned {successful} files[/green]",
|
|
200
|
+
)
|
|
201
|
+
if failed > 0:
|
|
202
|
+
self.console.print(f"[red]â Failed to clean {failed} files[/red]")
|
|
203
|
+
|
|
204
|
+
def _handle_cleaning_error(self, error: Exception) -> t.NoReturn:
|
|
205
|
+
"""Handle code cleaning errors."""
|
|
206
|
+
self.logger.error(f"Code cleaning failed: {error}")
|
|
207
|
+
raise CrackerjackError(
|
|
208
|
+
message=f"Code cleaning failed: {error}",
|
|
209
|
+
error_code=ErrorCode.CODE_CLEANING_ERROR,
|
|
210
|
+
) from error
|
|
211
|
+
|
|
212
|
+
def run_tests(
|
|
213
|
+
self,
|
|
214
|
+
coverage: bool = False,
|
|
215
|
+
workers: int | None = None,
|
|
216
|
+
timeout: int | None = None,
|
|
217
|
+
) -> TestResult:
|
|
218
|
+
import time
|
|
219
|
+
|
|
220
|
+
start_time = time.time()
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
self.logger.info("Running tests")
|
|
224
|
+
|
|
225
|
+
options = self._create_options(
|
|
226
|
+
test=True,
|
|
227
|
+
test_workers=workers or 0,
|
|
228
|
+
test_timeout=timeout or 0,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
success = asyncio.run(
|
|
232
|
+
self.orchestrator.pipeline.run_complete_workflow(options),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
duration = time.time() - start_time
|
|
236
|
+
|
|
237
|
+
return TestResult(
|
|
238
|
+
success=success,
|
|
239
|
+
passed_count=self._extract_test_passed_count(),
|
|
240
|
+
failed_count=self._extract_test_failed_count(),
|
|
241
|
+
coverage_percentage=self._extract_coverage_percentage(),
|
|
242
|
+
duration=duration,
|
|
243
|
+
errors=[] if success else ["Test execution failed"],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
duration = time.time() - start_time
|
|
248
|
+
self.logger.exception(f"Test execution failed: {e}")
|
|
249
|
+
|
|
250
|
+
return TestResult(
|
|
251
|
+
success=False,
|
|
252
|
+
passed_count=0,
|
|
253
|
+
failed_count=0,
|
|
254
|
+
coverage_percentage=0.0,
|
|
255
|
+
duration=duration,
|
|
256
|
+
errors=[str(e)],
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def publish_package(
|
|
260
|
+
self,
|
|
261
|
+
version_bump: str | None = None,
|
|
262
|
+
dry_run: bool = False,
|
|
263
|
+
) -> PublishResult:
|
|
264
|
+
try:
|
|
265
|
+
self.logger.info(
|
|
266
|
+
f"Publishing package (version_bump = {version_bump}, dry_run = {dry_run})",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
options = self._create_options(
|
|
270
|
+
bump=version_bump,
|
|
271
|
+
publish="pypi" if not dry_run else None,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
success = asyncio.run(
|
|
275
|
+
self.orchestrator.pipeline.run_complete_workflow(options),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return PublishResult(
|
|
279
|
+
success=success,
|
|
280
|
+
version=self._extract_current_version(),
|
|
281
|
+
published_to=["pypi"] if success and not dry_run else [],
|
|
282
|
+
errors=[] if success else ["Publishing failed"],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.logger.exception(f"Package publishing failed: {e}")
|
|
287
|
+
|
|
288
|
+
return PublishResult(
|
|
289
|
+
success=False,
|
|
290
|
+
version="",
|
|
291
|
+
published_to=[],
|
|
292
|
+
errors=[str(e)],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def run_interactive_workflow(
|
|
296
|
+
self,
|
|
297
|
+
options: InteractiveWorkflowOptions | None = None,
|
|
298
|
+
) -> bool:
|
|
299
|
+
options = options or InteractiveWorkflowOptions()
|
|
300
|
+
|
|
301
|
+
self.logger.info("Starting interactive workflow")
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
return self.interactive_cli.run_interactive_workflow(options)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
self.logger.exception(f"Interactive workflow failed: {e}")
|
|
307
|
+
self.console.print(f"[red]â Interactive workflow failed: {e}[/red]")
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def create_workflow_options(
|
|
311
|
+
self,
|
|
312
|
+
clean: bool = False,
|
|
313
|
+
test: bool = False,
|
|
314
|
+
publish: str | None = None,
|
|
315
|
+
bump: str | None = None,
|
|
316
|
+
commit: bool = False,
|
|
317
|
+
create_pr: bool = False,
|
|
318
|
+
**kwargs: t.Any,
|
|
319
|
+
) -> WorkflowOptions:
|
|
320
|
+
from .models.config import (
|
|
321
|
+
CleaningConfig,
|
|
322
|
+
ExecutionConfig,
|
|
323
|
+
GitConfig,
|
|
324
|
+
PublishConfig,
|
|
325
|
+
TestConfig,
|
|
326
|
+
)
|
|
327
|
+
from .models.config import (
|
|
328
|
+
WorkflowOptions as ModelsWorkflowOptions,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
verbose = kwargs.pop("verbose", False)
|
|
332
|
+
|
|
333
|
+
options = ModelsWorkflowOptions()
|
|
334
|
+
|
|
335
|
+
if clean:
|
|
336
|
+
options.cleaning = CleaningConfig(clean=True)
|
|
337
|
+
if test:
|
|
338
|
+
options.testing = TestConfig(test=True)
|
|
339
|
+
if publish or bump:
|
|
340
|
+
options.publishing = PublishConfig(publish=publish, bump=bump)
|
|
341
|
+
if commit or create_pr:
|
|
342
|
+
options.git = GitConfig(commit=commit, create_pr=create_pr)
|
|
343
|
+
if verbose:
|
|
344
|
+
options.execution = ExecutionConfig(verbose=True)
|
|
345
|
+
|
|
346
|
+
for key, value in kwargs.items():
|
|
347
|
+
if not hasattr(options, key):
|
|
348
|
+
setattr(options, key, value)
|
|
349
|
+
|
|
350
|
+
return options
|
|
351
|
+
|
|
352
|
+
def get_project_info(self) -> dict[str, t.Any]:
|
|
353
|
+
try:
|
|
354
|
+
pyproject_path = self.project_path / "pyproject.toml"
|
|
355
|
+
setup_py_path = self.project_path / "setup.py"
|
|
356
|
+
|
|
357
|
+
is_python_project = pyproject_path.exists() or setup_py_path.exists()
|
|
358
|
+
|
|
359
|
+
git_dir = self.project_path / ".git"
|
|
360
|
+
is_git_repo = git_dir.exists()
|
|
361
|
+
|
|
362
|
+
python_files = list(self.project_path.rglob("*.py"))
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"project_path": str(self.project_path),
|
|
366
|
+
"is_python_project": is_python_project,
|
|
367
|
+
"is_git_repo": is_git_repo,
|
|
368
|
+
"python_files_count": len(python_files),
|
|
369
|
+
"has_pyproject_toml": pyproject_path.exists(),
|
|
370
|
+
"has_setup_py": setup_py_path.exists(),
|
|
371
|
+
"has_requirements_txt": (
|
|
372
|
+
self.project_path / "requirements.txt"
|
|
373
|
+
).exists(),
|
|
374
|
+
"has_tests": any(self.project_path.rglob("test*.py")),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
self.logger.exception(f"Failed to get project info: {e}")
|
|
379
|
+
return {"error": str(e)}
|
|
380
|
+
|
|
381
|
+
def _create_options(self, **kwargs: t.Any) -> t.Any:
|
|
382
|
+
class Options:
|
|
383
|
+
def __init__(self, **kwargs: t.Any) -> None:
|
|
384
|
+
self.commit = False
|
|
385
|
+
self.interactive = False
|
|
386
|
+
self.no_config_updates = False
|
|
387
|
+
self.verbose = False
|
|
388
|
+
self.clean = False
|
|
389
|
+
self.test = False
|
|
390
|
+
self.autofix = True
|
|
391
|
+
self.publish = None
|
|
392
|
+
self.bump = None
|
|
393
|
+
self.test_workers = 0
|
|
394
|
+
self.test_timeout = 0
|
|
395
|
+
|
|
396
|
+
for key, value in kwargs.items():
|
|
397
|
+
setattr(self, key, value)
|
|
398
|
+
|
|
399
|
+
return Options(**kwargs)
|
|
400
|
+
|
|
401
|
+
def _extract_test_passed_count(self) -> int:
|
|
402
|
+
try:
|
|
403
|
+
test_manager = self.orchestrator.phases.test_manager
|
|
404
|
+
if hasattr(test_manager, "get_test_results"):
|
|
405
|
+
results = t.cast(t.Any, test_manager).get_test_results()
|
|
406
|
+
return getattr(results, "passed_count", 0)
|
|
407
|
+
return 0
|
|
408
|
+
except Exception:
|
|
409
|
+
return 0
|
|
410
|
+
|
|
411
|
+
def _extract_test_failed_count(self) -> int:
|
|
412
|
+
try:
|
|
413
|
+
test_manager = self.orchestrator.phases.test_manager
|
|
414
|
+
if hasattr(test_manager, "get_test_results"):
|
|
415
|
+
results = t.cast(t.Any, test_manager).get_test_results()
|
|
416
|
+
return getattr(results, "failed_count", 0)
|
|
417
|
+
return 0
|
|
418
|
+
except Exception:
|
|
419
|
+
return 0
|
|
420
|
+
|
|
421
|
+
def _extract_coverage_percentage(self) -> float:
|
|
422
|
+
try:
|
|
423
|
+
test_manager = self.orchestrator.phases.test_manager
|
|
424
|
+
if hasattr(test_manager, "get_test_results"):
|
|
425
|
+
results = t.cast(t.Any, test_manager).get_test_results()
|
|
426
|
+
return getattr(results, "coverage_percentage", 0.0)
|
|
427
|
+
return 0.0
|
|
428
|
+
except Exception:
|
|
429
|
+
return 0.0
|
|
430
|
+
|
|
431
|
+
def _extract_current_version(self) -> str:
|
|
432
|
+
try:
|
|
433
|
+
pyproject_path = self.project_path / "pyproject.toml"
|
|
434
|
+
if pyproject_path.exists():
|
|
435
|
+
import tomllib
|
|
436
|
+
|
|
437
|
+
with pyproject_path.open("rb") as f:
|
|
438
|
+
data = tomllib.load(f)
|
|
439
|
+
|
|
440
|
+
if "project" in data and "version" in data["project"]:
|
|
441
|
+
return data["project"]["version"]
|
|
442
|
+
if (
|
|
443
|
+
"tool" in data
|
|
444
|
+
and "poetry" in data["tool"]
|
|
445
|
+
and "version" in data["tool"]["poetry"]
|
|
446
|
+
):
|
|
447
|
+
return data["tool"]["poetry"]["version"]
|
|
448
|
+
|
|
449
|
+
import importlib.metadata
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
return importlib.metadata.version("crackerjack")
|
|
453
|
+
except importlib.metadata.PackageNotFoundError:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
return "unknown"
|
|
457
|
+
except Exception:
|
|
458
|
+
return "unknown"
|
|
459
|
+
|
|
460
|
+
def _check_for_todos(self, target_dir: Path) -> list[tuple[Path, int, str]]:
|
|
461
|
+
"""Check for TODO comments in Python files."""
|
|
462
|
+
import re
|
|
463
|
+
|
|
464
|
+
task_pattern = re.compile(f"#.*?{'T'}{'O'}{'D'}{'O'}.*", re.IGNORECASE)
|
|
465
|
+
python_files = self._get_python_files_for_todo_check(target_dir)
|
|
466
|
+
return self._scan_files_for_todos(python_files, task_pattern)
|
|
467
|
+
|
|
468
|
+
def _get_python_files_for_todo_check(self, target_dir: Path) -> list[Path]:
|
|
469
|
+
"""Get list of Python files to check for TODOs, excluding ignored directories."""
|
|
470
|
+
python_files: list[Path] = []
|
|
471
|
+
ignore_patterns = self._get_ignore_patterns()
|
|
472
|
+
|
|
473
|
+
for py_file in target_dir.rglob("*.py"):
|
|
474
|
+
if not self._should_skip_file(py_file, ignore_patterns):
|
|
475
|
+
python_files.append(py_file)
|
|
476
|
+
|
|
477
|
+
return python_files
|
|
478
|
+
|
|
479
|
+
def _get_ignore_patterns(self) -> set[str]:
|
|
480
|
+
"""Get patterns for directories/files to ignore during TODO scanning."""
|
|
481
|
+
return {
|
|
482
|
+
"__pycache__",
|
|
483
|
+
".git",
|
|
484
|
+
".venv",
|
|
485
|
+
"site-packages",
|
|
486
|
+
".pytest_cache",
|
|
487
|
+
"build",
|
|
488
|
+
"dist",
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def _should_skip_file(self, py_file: Path, ignore_patterns: set[str]) -> bool:
|
|
492
|
+
"""Check if a Python file should be skipped during TODO scanning."""
|
|
493
|
+
if py_file.name.startswith("."):
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
return any(parent.name in ignore_patterns for parent in py_file.parents)
|
|
497
|
+
|
|
498
|
+
def _scan_files_for_todos(
|
|
499
|
+
self,
|
|
500
|
+
python_files: list[Path],
|
|
501
|
+
todo_pattern: t.Any,
|
|
502
|
+
) -> list[tuple[Path, int, str]]:
|
|
503
|
+
"""Scan Python files for TODO comments."""
|
|
504
|
+
todos_found: list[tuple[Path, int, str]] = []
|
|
505
|
+
|
|
506
|
+
for file_path in python_files:
|
|
507
|
+
file_todos = self._scan_single_file_for_todos(file_path, todo_pattern)
|
|
508
|
+
todos_found.extend(file_todos)
|
|
509
|
+
|
|
510
|
+
return todos_found
|
|
511
|
+
|
|
512
|
+
def _scan_single_file_for_todos(
|
|
513
|
+
self,
|
|
514
|
+
file_path: Path,
|
|
515
|
+
todo_pattern: t.Any,
|
|
516
|
+
) -> list[tuple[Path, int, str]]:
|
|
517
|
+
"""Scan a single file for TODO comments."""
|
|
518
|
+
todos: list[tuple[Path, int, str]] = []
|
|
519
|
+
from contextlib import suppress
|
|
520
|
+
|
|
521
|
+
with suppress(UnicodeDecodeError, PermissionError):
|
|
522
|
+
with file_path.open() as f:
|
|
523
|
+
for line_no, line in enumerate(f, 1):
|
|
524
|
+
if todo_pattern.search(line):
|
|
525
|
+
todos.append((file_path, line_no, line))
|
|
526
|
+
|
|
527
|
+
return todos
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def run_quality_checks(
|
|
531
|
+
project_path: Path | None = None,
|
|
532
|
+
fast_only: bool = False,
|
|
533
|
+
autofix: bool = True,
|
|
534
|
+
) -> QualityCheckResult:
|
|
535
|
+
return CrackerjackAPI(project_path=project_path).run_quality_checks(
|
|
536
|
+
fast_only=fast_only,
|
|
537
|
+
autofix=autofix,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def clean_code(
|
|
542
|
+
project_path: Path | None = None,
|
|
543
|
+
backup: bool = True,
|
|
544
|
+
) -> list[CleaningResult]:
|
|
545
|
+
return CrackerjackAPI(project_path=project_path).clean_code(backup=backup)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def run_tests(project_path: Path | None = None, coverage: bool = False) -> TestResult:
|
|
549
|
+
return CrackerjackAPI(project_path=project_path).run_tests(coverage=coverage)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def publish_package(
|
|
553
|
+
project_path: Path | None = None,
|
|
554
|
+
version_bump: str | None = None,
|
|
555
|
+
dry_run: bool = False,
|
|
556
|
+
) -> PublishResult:
|
|
557
|
+
return CrackerjackAPI(project_path=project_path).publish_package(
|
|
558
|
+
version_bump=version_bump,
|
|
559
|
+
dry_run=dry_run,
|
|
560
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .handlers import (
|
|
2
|
+
handle_interactive_mode,
|
|
3
|
+
handle_monitor_mode,
|
|
4
|
+
handle_orchestrated_mode,
|
|
5
|
+
handle_standard_mode,
|
|
6
|
+
handle_watchdog_mode,
|
|
7
|
+
setup_ai_agent_env,
|
|
8
|
+
)
|
|
9
|
+
from .options import CLI_OPTIONS, BumpOption, Options, create_options
|
|
10
|
+
from .utils import get_package_version
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CLI_OPTIONS",
|
|
14
|
+
"BumpOption",
|
|
15
|
+
"Options",
|
|
16
|
+
"create_options",
|
|
17
|
+
"get_package_version",
|
|
18
|
+
"handle_interactive_mode",
|
|
19
|
+
"handle_monitor_mode",
|
|
20
|
+
"handle_orchestrated_mode",
|
|
21
|
+
"handle_standard_mode",
|
|
22
|
+
"handle_watchdog_mode",
|
|
23
|
+
"setup_ai_agent_env",
|
|
24
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""CLI Facade for backward compatibility.
|
|
2
|
+
|
|
3
|
+
This module provides a bridge between the existing CLI interface and the new
|
|
4
|
+
workflow orchestrator, ensuring all existing functionality continues to work.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from crackerjack.core.workflow_orchestrator import WorkflowOrchestrator
|
|
13
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CrackerjackCLIFacade:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
console: Console | None = None,
|
|
20
|
+
pkg_path: Path | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.console = console or Console(force_terminal=True)
|
|
23
|
+
self.pkg_path = pkg_path or Path.cwd()
|
|
24
|
+
self.orchestrator = WorkflowOrchestrator(
|
|
25
|
+
console=self.console,
|
|
26
|
+
pkg_path=self.pkg_path,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def process(self, options: OptionsProtocol) -> None:
|
|
30
|
+
try:
|
|
31
|
+
if self._should_handle_special_mode(options):
|
|
32
|
+
self._handle_special_modes(options)
|
|
33
|
+
return
|
|
34
|
+
success = asyncio.run(self.orchestrator.run_complete_workflow(options))
|
|
35
|
+
if not success:
|
|
36
|
+
self.console.print("[red]â Workflow completed with errors[/red]")
|
|
37
|
+
else:
|
|
38
|
+
self.console.print("[green]đ Workflow completed successfully![/green]")
|
|
39
|
+
except KeyboardInterrupt:
|
|
40
|
+
self.console.print("\n[yellow]âšī¸ Operation cancelled by user[/yellow]")
|
|
41
|
+
raise SystemExit(130)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.console.print(f"[red]đĨ Unexpected error: {e}[/red]")
|
|
44
|
+
if options.verbose:
|
|
45
|
+
import traceback
|
|
46
|
+
|
|
47
|
+
self.console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
48
|
+
raise SystemExit(1)
|
|
49
|
+
|
|
50
|
+
async def process_async(self, options: OptionsProtocol) -> None:
|
|
51
|
+
await asyncio.to_thread(self.process, options)
|
|
52
|
+
|
|
53
|
+
def _should_handle_special_mode(self, options: OptionsProtocol) -> bool:
|
|
54
|
+
return (
|
|
55
|
+
getattr(options, "start_mcp_server", False)
|
|
56
|
+
or getattr(options, "enterprise_batch", False)
|
|
57
|
+
or getattr(options, "monitor_dashboard", False)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _handle_special_modes(self, options: OptionsProtocol) -> None:
|
|
61
|
+
if getattr(options, "start_mcp_server", False):
|
|
62
|
+
self._start_mcp_server()
|
|
63
|
+
elif getattr(options, "enterprise_batch", False):
|
|
64
|
+
self._handle_enterprise_batch(options)
|
|
65
|
+
elif getattr(options, "monitor_dashboard", False):
|
|
66
|
+
self._handle_monitor_dashboard(options)
|
|
67
|
+
|
|
68
|
+
def _start_mcp_server(self) -> None:
|
|
69
|
+
try:
|
|
70
|
+
from crackerjack.mcp.server import main as start_mcp_main
|
|
71
|
+
|
|
72
|
+
self.console.print(
|
|
73
|
+
"[bold cyan]đ¤ Starting Crackerjack MCP Server...[/bold cyan]",
|
|
74
|
+
)
|
|
75
|
+
start_mcp_main(str(self.pkg_path))
|
|
76
|
+
except ImportError:
|
|
77
|
+
self.console.print(
|
|
78
|
+
"[red]â MCP server requires additional dependencies[/red]",
|
|
79
|
+
)
|
|
80
|
+
self.console.print("[yellow]Install with: uv sync --group mcp[/yellow]")
|
|
81
|
+
raise SystemExit(1)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.console.print(f"[red]â Failed to start MCP server: {e}[/red]")
|
|
84
|
+
raise SystemExit(1)
|
|
85
|
+
|
|
86
|
+
def _handle_enterprise_batch(self, options: OptionsProtocol) -> None:
|
|
87
|
+
self.console.print(
|
|
88
|
+
"[red]â Enterprise batch processing is not yet implemented[/red]"
|
|
89
|
+
)
|
|
90
|
+
raise SystemExit(1)
|
|
91
|
+
|
|
92
|
+
def _handle_monitor_dashboard(self, options: OptionsProtocol) -> None:
|
|
93
|
+
self.console.print("[red]â Monitoring dashboard is not yet implemented[/red]")
|
|
94
|
+
raise SystemExit(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_crackerjack_runner(
|
|
98
|
+
console: Console | None = None,
|
|
99
|
+
pkg_path: Path | None = None,
|
|
100
|
+
) -> CrackerjackCLIFacade:
|
|
101
|
+
return CrackerjackCLIFacade(console=console, pkg_path=pkg_path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
CrackerjackRunner = CrackerjackCLIFacade
|