crackerjack 0.32.0__py3-none-any.whl → 0.33.1__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/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +64 -6
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +257 -218
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +558 -240
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +66 -13
- crackerjack/managers/test_command_builder.py +5 -17
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +109 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +161 -32
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +174 -33
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +15 -12
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +3 -0
- crackerjack/mixins/error_handling.py +145 -0
- crackerjack/models/config.py +21 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +176 -107
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/models/task.py +3 -0
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +90 -105
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +18 -11
- crackerjack/services/config_merge.py +30 -85
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +41 -17
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +41 -45
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +30 -33
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +409 -0
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +416 -0
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +250 -576
- crackerjack/services/performance_cache.py +382 -0
- crackerjack/services/performance_monitor.py +565 -0
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +61 -30
- crackerjack/services/security_logger.py +18 -22
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.32.0.dist-info/RECORD +0 -180
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -202,7 +202,7 @@ class AgentRegistry:
|
|
|
202
202
|
|
|
203
203
|
def _build_agent_data(self, lines: list[str], yaml_end: int) -> dict[str, t.Any]:
|
|
204
204
|
yaml_lines = lines[1:yaml_end]
|
|
205
|
-
agent_data = {}
|
|
205
|
+
agent_data: dict[str, t.Any] = {}
|
|
206
206
|
|
|
207
207
|
for line in yaml_lines:
|
|
208
208
|
if ": " in line:
|
|
@@ -213,36 +213,46 @@ class AgentRegistry:
|
|
|
213
213
|
return agent_data
|
|
214
214
|
|
|
215
215
|
def _infer_capabilities_from_agent(self, agent: SubAgent) -> set[AgentCapability]:
|
|
216
|
-
capabilities
|
|
217
|
-
|
|
216
|
+
"""Infer agent capabilities from class name using keyword mapping."""
|
|
218
217
|
class_name = agent.__class__.__name__.lower()
|
|
218
|
+
capability_mapping = self._get_agent_capability_mapping()
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if "refactor" in class_name:
|
|
225
|
-
capabilities.add(AgentCapability.REFACTORING)
|
|
226
|
-
if "test" in class_name:
|
|
227
|
-
capabilities.add(AgentCapability.TESTING)
|
|
228
|
-
if "security" in class_name:
|
|
229
|
-
capabilities.add(AgentCapability.SECURITY)
|
|
230
|
-
if "performance" in class_name:
|
|
231
|
-
capabilities.add(AgentCapability.PERFORMANCE)
|
|
232
|
-
if "documentation" in class_name or "doc" in class_name:
|
|
233
|
-
capabilities.add(AgentCapability.DOCUMENTATION)
|
|
234
|
-
if "format" in class_name:
|
|
235
|
-
capabilities.add(AgentCapability.FORMATTING)
|
|
236
|
-
if "import" in class_name:
|
|
237
|
-
capabilities.add(AgentCapability.CODE_ANALYSIS)
|
|
238
|
-
if "dry" in class_name:
|
|
239
|
-
capabilities.add(AgentCapability.REFACTORING)
|
|
220
|
+
capabilities = set()
|
|
221
|
+
for keywords, caps in capability_mapping:
|
|
222
|
+
if self._class_name_matches_keywords(class_name, keywords):
|
|
223
|
+
capabilities.update(caps)
|
|
240
224
|
|
|
225
|
+
# Fallback to default capability if none found
|
|
241
226
|
if not capabilities:
|
|
242
227
|
capabilities.add(AgentCapability.CODE_ANALYSIS)
|
|
243
228
|
|
|
244
229
|
return capabilities
|
|
245
230
|
|
|
231
|
+
def _get_agent_capability_mapping(
|
|
232
|
+
self,
|
|
233
|
+
) -> list[tuple[list[str], set[AgentCapability]]]:
|
|
234
|
+
"""Get mapping of keywords to agent capabilities."""
|
|
235
|
+
return [
|
|
236
|
+
(
|
|
237
|
+
["architect"],
|
|
238
|
+
{AgentCapability.ARCHITECTURE, AgentCapability.CODE_ANALYSIS},
|
|
239
|
+
),
|
|
240
|
+
(["refactor"], {AgentCapability.REFACTORING}),
|
|
241
|
+
(["test"], {AgentCapability.TESTING}),
|
|
242
|
+
(["security"], {AgentCapability.SECURITY}),
|
|
243
|
+
(["performance"], {AgentCapability.PERFORMANCE}),
|
|
244
|
+
(["documentation", "doc"], {AgentCapability.DOCUMENTATION}),
|
|
245
|
+
(["format"], {AgentCapability.FORMATTING}),
|
|
246
|
+
(["import"], {AgentCapability.CODE_ANALYSIS}),
|
|
247
|
+
(["dry"], {AgentCapability.REFACTORING}),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
def _class_name_matches_keywords(
|
|
251
|
+
self, class_name: str, keywords: list[str]
|
|
252
|
+
) -> bool:
|
|
253
|
+
"""Check if class name contains any of the specified keywords."""
|
|
254
|
+
return any(keyword in class_name for keyword in keywords)
|
|
255
|
+
|
|
246
256
|
def _infer_capabilities_from_user_agent(
|
|
247
257
|
self, agent_data: dict[str, t.Any]
|
|
248
258
|
) -> set[AgentCapability]:
|
|
@@ -334,7 +344,7 @@ class AgentRegistry:
|
|
|
334
344
|
return self._agents.get(name)
|
|
335
345
|
|
|
336
346
|
def list_all_agents(self) -> list[RegisteredAgent]:
|
|
337
|
-
agents = list(self._agents.values())
|
|
347
|
+
agents = list[t.Any](self._agents.values())
|
|
338
348
|
agents.sort(key=lambda a: a.metadata.priority, reverse=True)
|
|
339
349
|
return agents
|
|
340
350
|
|
|
@@ -143,9 +143,7 @@ class AgentSelector:
|
|
|
143
143
|
|
|
144
144
|
capabilities = set()
|
|
145
145
|
for pattern, caps in self._task_patterns.items():
|
|
146
|
-
if re.search(
|
|
147
|
-
pattern, text, re.IGNORECASE
|
|
148
|
-
): # REGEX OK: dynamic pattern matching from config
|
|
146
|
+
if re.search(pattern, text, re.IGNORECASE):
|
|
149
147
|
capabilities.update(caps)
|
|
150
148
|
|
|
151
149
|
return capabilities
|
|
@@ -182,7 +180,7 @@ class AgentSelector:
|
|
|
182
180
|
TaskContext.GENERAL: [AgentCapability.CODE_ANALYSIS],
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
return set(context_map.get(task.context, []))
|
|
183
|
+
return set[t.Any](context_map.get(task.context, []))
|
|
186
184
|
|
|
187
185
|
def _analyze_file_patterns(self, task: TaskDescription) -> set[AgentCapability]:
|
|
188
186
|
if not task.file_patterns:
|
|
@@ -291,8 +289,8 @@ class AgentSelector:
|
|
|
291
289
|
if not agent.metadata.description:
|
|
292
290
|
return 0.0
|
|
293
291
|
|
|
294
|
-
desc_words = set(agent.metadata.description.lower().split())
|
|
295
|
-
task_words = set(task_text.split())
|
|
292
|
+
desc_words = set[t.Any](agent.metadata.description.lower().split())
|
|
293
|
+
task_words = set[t.Any](task_text.split())
|
|
296
294
|
common_words = desc_words & task_words
|
|
297
295
|
|
|
298
296
|
if common_words:
|
|
@@ -355,7 +353,7 @@ class AgentSelector:
|
|
|
355
353
|
parts.append(source_desc.get(agent.metadata.source.value, "Unknown source"))
|
|
356
354
|
|
|
357
355
|
if agent.metadata.capabilities:
|
|
358
|
-
top_caps = list(agent.metadata.capabilities)[:2]
|
|
356
|
+
top_caps = list[t.Any](agent.metadata.capabilities)[:2]
|
|
359
357
|
cap_names = [cap.value.replace("_", " ") for cap in top_caps]
|
|
360
358
|
parts.append(f"Strengths: {', '.join(cap_names)}")
|
|
361
359
|
|
crackerjack/interactive.py
CHANGED
|
@@ -37,12 +37,23 @@ class WorkflowOptions:
|
|
|
37
37
|
@classmethod
|
|
38
38
|
def from_args(cls, args: t.Any) -> "WorkflowOptions":
|
|
39
39
|
return cls(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
cleaning=CleaningConfig(
|
|
41
|
+
clean=getattr(args, "clean", False),
|
|
42
|
+
),
|
|
43
|
+
testing=TestConfig(
|
|
44
|
+
test=getattr(args, "test", False),
|
|
45
|
+
),
|
|
46
|
+
publishing=PublishConfig(
|
|
47
|
+
publish=getattr(args, "publish", None),
|
|
48
|
+
bump=getattr(args, "bump", None),
|
|
49
|
+
),
|
|
50
|
+
git=GitConfig(
|
|
51
|
+
commit=getattr(args, "commit", False),
|
|
52
|
+
create_pr=getattr(args, "create_pr", False),
|
|
53
|
+
),
|
|
54
|
+
ai=AIConfig(
|
|
55
|
+
ai_agent=getattr(args, "ai_agent", False),
|
|
56
|
+
),
|
|
46
57
|
interactive=getattr(args, "interactive", True),
|
|
47
58
|
dry_run=getattr(args, "dry_run", False),
|
|
48
59
|
)
|
|
@@ -28,7 +28,6 @@ class AsyncHookManager:
|
|
|
28
28
|
self._config_path: Path | None = None
|
|
29
29
|
|
|
30
30
|
def set_config_path(self, config_path: Path) -> None:
|
|
31
|
-
"""Set the path to the pre-commit configuration file."""
|
|
32
31
|
self._config_path = config_path
|
|
33
32
|
|
|
34
33
|
async def run_fast_hooks_async(self) -> list[HookResult]:
|
|
@@ -6,6 +6,7 @@ from rich.console import Console
|
|
|
6
6
|
|
|
7
7
|
from crackerjack.config.hooks import HookConfigLoader
|
|
8
8
|
from crackerjack.executors.hook_executor import HookExecutor
|
|
9
|
+
from crackerjack.executors.lsp_aware_hook_executor import LSPAwareHookExecutor
|
|
9
10
|
from crackerjack.models.task import HookResult
|
|
10
11
|
|
|
11
12
|
|
|
@@ -16,12 +17,25 @@ class HookManagerImpl:
|
|
|
16
17
|
pkg_path: Path,
|
|
17
18
|
verbose: bool = False,
|
|
18
19
|
quiet: bool = False,
|
|
20
|
+
enable_lsp_optimization: bool = False,
|
|
21
|
+
enable_tool_proxy: bool = True,
|
|
19
22
|
) -> None:
|
|
20
23
|
self.console = console
|
|
21
24
|
self.pkg_path = pkg_path
|
|
22
|
-
self.executor
|
|
25
|
+
self.executor: HookExecutor
|
|
26
|
+
|
|
27
|
+
# Use LSP-aware executor if optimization is enabled
|
|
28
|
+
if enable_lsp_optimization:
|
|
29
|
+
self.executor = LSPAwareHookExecutor(
|
|
30
|
+
console, pkg_path, verbose, quiet, use_tool_proxy=enable_tool_proxy
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
self.executor = HookExecutor(console, pkg_path, verbose, quiet)
|
|
34
|
+
|
|
23
35
|
self.config_loader = HookConfigLoader()
|
|
24
36
|
self._config_path: Path | None = None
|
|
37
|
+
self.lsp_optimization_enabled = enable_lsp_optimization
|
|
38
|
+
self.tool_proxy_enabled = enable_tool_proxy
|
|
25
39
|
|
|
26
40
|
def set_config_path(self, config_path: Path) -> None:
|
|
27
41
|
self._config_path = config_path
|
|
@@ -49,6 +63,70 @@ class HookManagerImpl:
|
|
|
49
63
|
comprehensive_results = self.run_comprehensive_hooks()
|
|
50
64
|
return fast_results + comprehensive_results
|
|
51
65
|
|
|
66
|
+
def get_execution_info(self) -> dict[str, t.Any]:
|
|
67
|
+
"""Get information about current execution mode and capabilities."""
|
|
68
|
+
info = {
|
|
69
|
+
"lsp_optimization_enabled": self.lsp_optimization_enabled,
|
|
70
|
+
"tool_proxy_enabled": self.tool_proxy_enabled,
|
|
71
|
+
"executor_type": type(self.executor).__name__,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Get LSP-specific info if available
|
|
75
|
+
if hasattr(self.executor, "get_execution_mode_summary"):
|
|
76
|
+
info.update(self.executor.get_execution_mode_summary())
|
|
77
|
+
|
|
78
|
+
return info
|
|
79
|
+
|
|
80
|
+
def configure_lsp_optimization(self, enable: bool) -> None:
|
|
81
|
+
"""Enable or disable LSP optimization by switching executors."""
|
|
82
|
+
if enable == self.lsp_optimization_enabled:
|
|
83
|
+
return # Already in the correct state
|
|
84
|
+
|
|
85
|
+
# Switch executor based on the enable flag
|
|
86
|
+
if enable:
|
|
87
|
+
self.executor = LSPAwareHookExecutor(
|
|
88
|
+
self.console,
|
|
89
|
+
self.pkg_path,
|
|
90
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
91
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
92
|
+
use_tool_proxy=self.tool_proxy_enabled,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
self.executor = HookExecutor(
|
|
96
|
+
self.console,
|
|
97
|
+
self.pkg_path,
|
|
98
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
99
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.lsp_optimization_enabled = enable
|
|
103
|
+
|
|
104
|
+
# Restore config path if it was set[t.Any]
|
|
105
|
+
if self._config_path:
|
|
106
|
+
# Config path is set[t.Any] at the manager level, not executor level
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def configure_tool_proxy(self, enable: bool) -> None:
|
|
110
|
+
"""Enable or disable tool proxy resilience."""
|
|
111
|
+
if enable == self.tool_proxy_enabled:
|
|
112
|
+
return # Already in the correct state
|
|
113
|
+
|
|
114
|
+
self.tool_proxy_enabled = enable
|
|
115
|
+
|
|
116
|
+
# If using LSP-aware executor, recreate it with new tool proxy setting
|
|
117
|
+
if isinstance(self.executor, LSPAwareHookExecutor):
|
|
118
|
+
self.executor = LSPAwareHookExecutor(
|
|
119
|
+
self.console,
|
|
120
|
+
self.pkg_path,
|
|
121
|
+
verbose=getattr(self.executor, "verbose", False),
|
|
122
|
+
quiet=getattr(self.executor, "quiet", True),
|
|
123
|
+
use_tool_proxy=enable,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Restore config path if it was set[t.Any]
|
|
127
|
+
if self._config_path:
|
|
128
|
+
pass # Config path handled at manager level
|
|
129
|
+
|
|
52
130
|
def validate_hooks_config(self) -> bool:
|
|
53
131
|
try:
|
|
54
132
|
result = subprocess.run(
|
|
@@ -5,17 +5,34 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from crackerjack.
|
|
9
|
-
from crackerjack.services.security import SecurityService
|
|
8
|
+
from crackerjack.models.protocols import FileSystemInterface, SecurityServiceProtocol
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class PublishManagerImpl:
|
|
13
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
console: Console,
|
|
15
|
+
pkg_path: Path,
|
|
16
|
+
dry_run: bool = False,
|
|
17
|
+
filesystem: FileSystemInterface | None = None,
|
|
18
|
+
security: SecurityServiceProtocol | None = None,
|
|
19
|
+
) -> None:
|
|
14
20
|
self.console = console
|
|
15
21
|
self.pkg_path = pkg_path
|
|
16
22
|
self.dry_run = dry_run
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
|
|
24
|
+
if filesystem is None:
|
|
25
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
26
|
+
|
|
27
|
+
filesystem = FileSystemService()
|
|
28
|
+
|
|
29
|
+
if security is None:
|
|
30
|
+
from crackerjack.services.security import SecurityService
|
|
31
|
+
|
|
32
|
+
security = SecurityService()
|
|
33
|
+
|
|
34
|
+
self.filesystem = filesystem
|
|
35
|
+
self.security = security
|
|
19
36
|
|
|
20
37
|
def _run_command(
|
|
21
38
|
self,
|
|
@@ -50,7 +67,8 @@ class PublishManagerImpl:
|
|
|
50
67
|
|
|
51
68
|
content = self.filesystem.read_file(pyproject_path)
|
|
52
69
|
data = loads(content)
|
|
53
|
-
|
|
70
|
+
version = data.get("project", {}).get("version")
|
|
71
|
+
return version if isinstance(version, str) else None
|
|
54
72
|
except Exception as e:
|
|
55
73
|
self.console.print(f"[yellow]⚠️[/ yellow] Error reading version: {e}")
|
|
56
74
|
return None
|
|
@@ -117,6 +135,8 @@ class PublishManagerImpl:
|
|
|
117
135
|
self.console.print(
|
|
118
136
|
f"[green]🚀[/ green] Bumped {version_type} version: {current_version} → {new_version}",
|
|
119
137
|
)
|
|
138
|
+
# Update changelog after successful version bump
|
|
139
|
+
self._update_changelog_for_version(current_version, new_version)
|
|
120
140
|
else:
|
|
121
141
|
msg = "Failed to update version in file"
|
|
122
142
|
raise ValueError(msg)
|
|
@@ -180,7 +200,7 @@ class PublishManagerImpl:
|
|
|
180
200
|
[
|
|
181
201
|
"keyring",
|
|
182
202
|
"get",
|
|
183
|
-
"https
|
|
203
|
+
"https: //upload.pypi.org/legacy/",
|
|
184
204
|
"__token__",
|
|
185
205
|
],
|
|
186
206
|
)
|
|
@@ -211,7 +231,7 @@ class PublishManagerImpl:
|
|
|
211
231
|
" 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
|
|
212
232
|
)
|
|
213
233
|
self.console.print(
|
|
214
|
-
" 2. Use keyring: keyring set https
|
|
234
|
+
" 2. Use keyring: keyring set[t.Any] https: //upload.pypi.org/legacy/ __token__",
|
|
215
235
|
)
|
|
216
236
|
self.console.print(
|
|
217
237
|
" 3. Ensure token starts with 'pypi-' and is properly formatted",
|
|
@@ -269,7 +289,7 @@ class PublishManagerImpl:
|
|
|
269
289
|
if not dist_dir.exists():
|
|
270
290
|
return
|
|
271
291
|
|
|
272
|
-
artifacts = list(dist_dir.glob("*"))
|
|
292
|
+
artifacts = list[t.Any](dist_dir.glob("*"))
|
|
273
293
|
self.console.print(f"[cyan]📦[/ cyan] Build artifacts ({len(artifacts)}): ")
|
|
274
294
|
|
|
275
295
|
for artifact in artifacts[-5:]:
|
|
@@ -278,8 +298,8 @@ class PublishManagerImpl:
|
|
|
278
298
|
|
|
279
299
|
def _format_file_size(self, size: int) -> str:
|
|
280
300
|
if size < 1024 * 1024:
|
|
281
|
-
return f"{size / 1024
|
|
282
|
-
return f"{size / (1024 * 1024)
|
|
301
|
+
return f"{size / 1024: .1f}KB"
|
|
302
|
+
return f"{size / (1024 * 1024): .1f}MB"
|
|
283
303
|
|
|
284
304
|
def publish_package(self) -> bool:
|
|
285
305
|
if not self._validate_prerequisites():
|
|
@@ -330,7 +350,7 @@ class PublishManagerImpl:
|
|
|
330
350
|
package_name = self._get_package_name()
|
|
331
351
|
|
|
332
352
|
if package_name and current_version:
|
|
333
|
-
url = f"https
|
|
353
|
+
url = f"https: //pypi.org/project/{package_name}/{current_version}/"
|
|
334
354
|
self.console.print(f"[cyan]🔗[/ cyan] Package URL: {url}")
|
|
335
355
|
|
|
336
356
|
def _get_package_name(self) -> str | None:
|
|
@@ -341,7 +361,8 @@ class PublishManagerImpl:
|
|
|
341
361
|
|
|
342
362
|
content = self.filesystem.read_file(pyproject_path)
|
|
343
363
|
data = loads(content)
|
|
344
|
-
|
|
364
|
+
name = data.get("project", {}).get("name", "")
|
|
365
|
+
return name if isinstance(name, str) else None
|
|
345
366
|
|
|
346
367
|
return None
|
|
347
368
|
|
|
@@ -429,3 +450,35 @@ class PublishManagerImpl:
|
|
|
429
450
|
except Exception as e:
|
|
430
451
|
self.console.print(f"[yellow]⚠️[/ yellow] Error reading package info: {e}")
|
|
431
452
|
return {}
|
|
453
|
+
|
|
454
|
+
def _update_changelog_for_version(self, old_version: str, new_version: str) -> None:
|
|
455
|
+
"""Update changelog with entries from git commits since last version."""
|
|
456
|
+
try:
|
|
457
|
+
from crackerjack.services.changelog_automation import ChangelogGenerator
|
|
458
|
+
from crackerjack.services.git import GitService
|
|
459
|
+
|
|
460
|
+
# Initialize services
|
|
461
|
+
git_service = GitService(self.console, self.pkg_path)
|
|
462
|
+
changelog_generator = ChangelogGenerator(self.console, git_service)
|
|
463
|
+
|
|
464
|
+
# Look for changelog file
|
|
465
|
+
changelog_path = self.pkg_path / "CHANGELOG.md"
|
|
466
|
+
|
|
467
|
+
# Generate changelog entries since last version
|
|
468
|
+
success = changelog_generator.generate_changelog_from_commits(
|
|
469
|
+
changelog_path=changelog_path,
|
|
470
|
+
version=new_version,
|
|
471
|
+
since_version=f"v{old_version}", # Assumes git tags are prefixed with 'v'
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if success:
|
|
475
|
+
self.console.print(
|
|
476
|
+
f"[green]📝[/green] Updated changelog for version {new_version}"
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
self.console.print(
|
|
480
|
+
"[yellow]⚠️[/yellow] Changelog update encountered issues"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
self.console.print(f"[yellow]⚠️[/yellow] Failed to update changelog: {e}")
|
|
@@ -8,7 +8,7 @@ class TestCommandBuilder:
|
|
|
8
8
|
self.pkg_path = pkg_path
|
|
9
9
|
|
|
10
10
|
def build_command(self, options: OptionsProtocol) -> list[str]:
|
|
11
|
-
cmd = ["python", "-m", "pytest"]
|
|
11
|
+
cmd = ["uv", "run", "python", "-m", "pytest"]
|
|
12
12
|
|
|
13
13
|
self._add_coverage_options(cmd, options)
|
|
14
14
|
self._add_worker_options(cmd, options)
|
|
@@ -23,22 +23,8 @@ class TestCommandBuilder:
|
|
|
23
23
|
if hasattr(options, "test_workers") and options.test_workers:
|
|
24
24
|
return options.test_workers
|
|
25
25
|
|
|
26
|
-
# Temporarily disable multi-worker execution due to pytest-xdist
|
|
27
|
-
# hanging issues with async tests. See GitHub issue for details.
|
|
28
|
-
# TODO: Re-enable after fixing async test timeout issues
|
|
29
26
|
return 1
|
|
30
27
|
|
|
31
|
-
# Original multi-worker logic (commented out):
|
|
32
|
-
# import multiprocessing
|
|
33
|
-
# cpu_count = multiprocessing.cpu_count()
|
|
34
|
-
# if cpu_count <= 2:
|
|
35
|
-
# return 1
|
|
36
|
-
# elif cpu_count <= 4:
|
|
37
|
-
# return 2
|
|
38
|
-
# elif cpu_count <= 8:
|
|
39
|
-
# return 3
|
|
40
|
-
# return 4
|
|
41
|
-
|
|
42
28
|
def get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
43
29
|
if hasattr(options, "test_timeout") and options.test_timeout:
|
|
44
30
|
return options.test_timeout
|
|
@@ -68,7 +54,7 @@ class TestCommandBuilder:
|
|
|
68
54
|
[
|
|
69
55
|
"--benchmark-only",
|
|
70
56
|
"--benchmark-sort=mean",
|
|
71
|
-
"--benchmark-columns=min,max,mean,stddev",
|
|
57
|
+
"--benchmark-columns=min, max, mean, stddev",
|
|
72
58
|
]
|
|
73
59
|
)
|
|
74
60
|
|
|
@@ -99,7 +85,7 @@ class TestCommandBuilder:
|
|
|
99
85
|
cmd.append(str(self.pkg_path))
|
|
100
86
|
|
|
101
87
|
def build_specific_test_command(self, test_pattern: str) -> list[str]:
|
|
102
|
-
cmd = ["python", "-m", "pytest", "-v"]
|
|
88
|
+
cmd = ["uv", "run", "python", "-m", "pytest", "-v"]
|
|
103
89
|
|
|
104
90
|
cmd.extend(
|
|
105
91
|
[
|
|
@@ -116,6 +102,8 @@ class TestCommandBuilder:
|
|
|
116
102
|
|
|
117
103
|
def build_validation_command(self) -> list[str]:
|
|
118
104
|
return [
|
|
105
|
+
"uv",
|
|
106
|
+
"run",
|
|
119
107
|
"python",
|
|
120
108
|
"-m",
|
|
121
109
|
"pytest",
|
|
@@ -189,9 +189,7 @@ class TestExecutor:
|
|
|
189
189
|
|
|
190
190
|
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
191
191
|
if "collected" in line and ("item" in line or "test" in line):
|
|
192
|
-
match = re.search(
|
|
193
|
-
r"(\d +) (?: item | test)", line
|
|
194
|
-
) # REGEX OK: parsing pytest output format
|
|
192
|
+
match = re.search(r"(\d +) (?: item | test)", line)
|
|
195
193
|
if match:
|
|
196
194
|
progress.update(
|
|
197
195
|
total_tests=int(match.group(1)),
|
|
@@ -5,25 +5,44 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
-
from crackerjack.models.protocols import OptionsProtocol
|
|
9
|
-
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
8
|
+
from crackerjack.models.protocols import CoverageRatchetProtocol, OptionsProtocol
|
|
10
9
|
|
|
11
10
|
from .test_command_builder import TestCommandBuilder
|
|
12
11
|
from .test_executor import TestExecutor
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class TestManager:
|
|
16
|
-
def __init__(
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
console: Console,
|
|
18
|
+
pkg_path: Path,
|
|
19
|
+
coverage_ratchet: CoverageRatchetProtocol | None = None,
|
|
20
|
+
) -> None:
|
|
17
21
|
self.console = console
|
|
18
22
|
self.pkg_path = pkg_path
|
|
19
23
|
|
|
20
24
|
self.executor = TestExecutor(console, pkg_path)
|
|
21
25
|
self.command_builder = TestCommandBuilder(pkg_path)
|
|
22
|
-
|
|
26
|
+
|
|
27
|
+
if coverage_ratchet is None:
|
|
28
|
+
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
29
|
+
|
|
30
|
+
coverage_ratchet_obj = CoverageRatchetService(pkg_path, console)
|
|
31
|
+
self.coverage_ratchet: CoverageRatchetProtocol | None = t.cast(
|
|
32
|
+
CoverageRatchetProtocol, coverage_ratchet_obj
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
self.coverage_ratchet = coverage_ratchet
|
|
23
36
|
|
|
24
37
|
self._last_test_failures: list[str] = []
|
|
25
38
|
self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
|
|
26
39
|
self.coverage_ratchet_enabled = True
|
|
40
|
+
self.use_lsp_diagnostics = True
|
|
41
|
+
|
|
42
|
+
# Initialize coverage badge service
|
|
43
|
+
from crackerjack.services.coverage_badge_service import CoverageBadgeService
|
|
44
|
+
|
|
45
|
+
self._coverage_badge_service = CoverageBadgeService(console, pkg_path)
|
|
27
46
|
|
|
28
47
|
def set_progress_callback(
|
|
29
48
|
self,
|
|
@@ -145,11 +164,11 @@ class TestManager:
|
|
|
145
164
|
test_path = self.pkg_path / test_dir
|
|
146
165
|
if test_path.exists() and test_path.is_dir():
|
|
147
166
|
for test_file_pattern in test_files:
|
|
148
|
-
if list(test_path.glob(f"**/{test_file_pattern}")):
|
|
167
|
+
if list[t.Any](test_path.glob(f"**/{test_file_pattern}")):
|
|
149
168
|
return True
|
|
150
169
|
|
|
151
170
|
for test_file_pattern in test_files:
|
|
152
|
-
if list(self.pkg_path.glob(test_file_pattern)):
|
|
171
|
+
if list[t.Any](self.pkg_path.glob(test_file_pattern)):
|
|
153
172
|
return True
|
|
154
173
|
|
|
155
174
|
return False
|
|
@@ -201,8 +220,44 @@ class TestManager:
|
|
|
201
220
|
return True
|
|
202
221
|
|
|
203
222
|
ratchet_result = self.coverage_ratchet.check_and_update_coverage()
|
|
223
|
+
|
|
224
|
+
# Update coverage badge if coverage information is available
|
|
225
|
+
self._update_coverage_badge(ratchet_result)
|
|
226
|
+
|
|
204
227
|
return self._handle_ratchet_result(ratchet_result)
|
|
205
228
|
|
|
229
|
+
def _update_coverage_badge(self, ratchet_result: dict[str, t.Any]) -> None:
|
|
230
|
+
"""Update coverage badge in README.md if coverage changed."""
|
|
231
|
+
try:
|
|
232
|
+
# Get current coverage directly from coverage.json to ensure freshest data
|
|
233
|
+
import json
|
|
234
|
+
|
|
235
|
+
current_coverage = None
|
|
236
|
+
coverage_json_path = self.pkg_path / "coverage.json"
|
|
237
|
+
|
|
238
|
+
if coverage_json_path.exists():
|
|
239
|
+
with coverage_json_path.open() as f:
|
|
240
|
+
data = json.load(f)
|
|
241
|
+
current_coverage = data.get("totals", {}).get("percent_covered")
|
|
242
|
+
|
|
243
|
+
# Fallback to ratchet result if coverage.json not available
|
|
244
|
+
if current_coverage is None:
|
|
245
|
+
current_coverage = ratchet_result.get("current_coverage")
|
|
246
|
+
|
|
247
|
+
# Final fallback to coverage service
|
|
248
|
+
if current_coverage is None:
|
|
249
|
+
coverage_info = self.get_coverage()
|
|
250
|
+
current_coverage = coverage_info.get("coverage_percent")
|
|
251
|
+
|
|
252
|
+
if current_coverage is not None:
|
|
253
|
+
if self._coverage_badge_service.should_update_badge(current_coverage):
|
|
254
|
+
self._coverage_badge_service.update_readme_coverage_badge(
|
|
255
|
+
current_coverage
|
|
256
|
+
)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
# Don't fail the test process if badge update fails
|
|
259
|
+
self.console.print(f"[yellow]⚠️[/yellow] Badge update failed: {e}")
|
|
260
|
+
|
|
206
261
|
def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
|
|
207
262
|
if ratchet_result.get("success", False):
|
|
208
263
|
if ratchet_result.get("improved", False):
|
|
@@ -216,7 +271,7 @@ class TestManager:
|
|
|
216
271
|
previous = ratchet_result.get("previous_coverage", 0)
|
|
217
272
|
self.console.print(
|
|
218
273
|
f"[red]📉[/ red] Coverage regression: "
|
|
219
|
-
f"{current
|
|
274
|
+
f"{current: .2f}% < {previous: .2f}%"
|
|
220
275
|
)
|
|
221
276
|
return False
|
|
222
277
|
|
|
@@ -244,5 +299,52 @@ class TestManager:
|
|
|
244
299
|
def _get_timeout(self, options: OptionsProtocol) -> int:
|
|
245
300
|
return self.command_builder.get_test_timeout(options)
|
|
246
301
|
|
|
302
|
+
async def run_pre_test_lsp_diagnostics(self) -> bool:
|
|
303
|
+
"""Run LSP diagnostics before tests to catch type errors early."""
|
|
304
|
+
if not self.use_lsp_diagnostics:
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
from crackerjack.services.lsp_client import LSPClient
|
|
309
|
+
|
|
310
|
+
lsp_client = LSPClient(self.console)
|
|
311
|
+
|
|
312
|
+
# Check if LSP server is available
|
|
313
|
+
if not lsp_client.is_server_running():
|
|
314
|
+
return True # No LSP server, skip diagnostics
|
|
315
|
+
|
|
316
|
+
# Run type diagnostics on the project
|
|
317
|
+
diagnostics, summary = lsp_client.check_project_with_feedback(
|
|
318
|
+
self.pkg_path,
|
|
319
|
+
show_progress=False, # Keep quiet for test integration
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Check if there are type errors
|
|
323
|
+
has_errors = any(diags for diags in diagnostics.values())
|
|
324
|
+
|
|
325
|
+
if has_errors:
|
|
326
|
+
self.console.print(
|
|
327
|
+
"[yellow]⚠️ LSP detected type errors before running tests[/yellow]"
|
|
328
|
+
)
|
|
329
|
+
# Format and show a summary
|
|
330
|
+
error_count = sum(len(diags) for diags in diagnostics.values())
|
|
331
|
+
self.console.print(f"[yellow]Found {error_count} type issues[/yellow]")
|
|
332
|
+
|
|
333
|
+
return not has_errors # Return False if there are type errors
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
# If LSP diagnostics fail, don't block tests
|
|
337
|
+
self.console.print(f"[dim]LSP diagnostics failed: {e}[/dim]")
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
def configure_lsp_diagnostics(self, enable: bool) -> None:
|
|
341
|
+
"""Enable or disable LSP diagnostics integration."""
|
|
342
|
+
self.use_lsp_diagnostics = enable
|
|
343
|
+
|
|
344
|
+
if enable:
|
|
345
|
+
self.console.print(
|
|
346
|
+
"[cyan]🔍 LSP diagnostics enabled for faster test feedback[/cyan]"
|
|
347
|
+
)
|
|
348
|
+
|
|
247
349
|
|
|
248
350
|
TestManagementImpl = TestManager
|