crackerjack 0.33.0__py3-none-any.whl → 0.33.2__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 +4 -13
- 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 +104 -204
- 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 +171 -174
- 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 +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -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 +17 -16
- 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 +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- 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 +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- 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 +6 -35
- 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 +15 -9
- crackerjack/services/config_merge.py +19 -80
- 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 +27 -25
- 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 +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- 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 +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- 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 +618 -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 +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- 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.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import subprocess
|
|
3
|
+
import typing as t
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import tomli
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ConfigUpdateInfo:
|
|
16
|
+
config_type: str
|
|
17
|
+
current_version: str
|
|
18
|
+
latest_version: str
|
|
19
|
+
needs_update: bool
|
|
20
|
+
diff_preview: str = ""
|
|
21
|
+
last_updated: datetime | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConfigVersion:
|
|
26
|
+
version: str
|
|
27
|
+
config_data: dict[str, t.Any]
|
|
28
|
+
dependencies: list[str] = field(default_factory=list)
|
|
29
|
+
description: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfigTemplateService:
|
|
33
|
+
"""Version-based configuration template management service."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, console: Console, pkg_path: Path) -> None:
|
|
36
|
+
self.console = console
|
|
37
|
+
self.pkg_path = pkg_path
|
|
38
|
+
self.templates = self._load_config_templates()
|
|
39
|
+
|
|
40
|
+
def _load_config_templates(self) -> dict[str, ConfigVersion]:
|
|
41
|
+
"""Load configuration templates as structured data."""
|
|
42
|
+
return {
|
|
43
|
+
"pre-commit": self._create_precommit_template(),
|
|
44
|
+
"pyproject": self._create_pyproject_template(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def _create_precommit_template(self) -> ConfigVersion:
|
|
48
|
+
"""Create pre-commit configuration template."""
|
|
49
|
+
return ConfigVersion(
|
|
50
|
+
version="3.0.0",
|
|
51
|
+
description="Pre-commit hooks configuration with modern tools",
|
|
52
|
+
config_data={"repos": self._build_precommit_repos()},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _create_pyproject_template(self) -> ConfigVersion:
|
|
56
|
+
"""Create pyproject.toml configuration template."""
|
|
57
|
+
return ConfigVersion(
|
|
58
|
+
version="1.2.0",
|
|
59
|
+
description="Modern Python project configuration with Ruff and pytest",
|
|
60
|
+
config_data={"tool": self._build_pyproject_tools()},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _build_precommit_repos(self) -> list[dict[str, t.Any]]:
|
|
64
|
+
"""Build pre-commit repository configurations."""
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
"repo": "local",
|
|
68
|
+
"hooks": self._build_local_precommit_hooks(),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"repo": "https://github.com/pre-commit/pre-commit-hooks",
|
|
72
|
+
"rev": "v6.0.0",
|
|
73
|
+
"hooks": self._build_standard_precommit_hooks(),
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def _build_local_precommit_hooks(self) -> list[dict[str, t.Any]]:
|
|
78
|
+
"""Build local pre-commit hook configurations."""
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
"id": "validate-regex-patterns",
|
|
82
|
+
"name": "validate-regex-patterns",
|
|
83
|
+
"entry": "uv run python -m crackerjack.tools.validate_regex_patterns",
|
|
84
|
+
"language": "system",
|
|
85
|
+
"files": r"\.py$",
|
|
86
|
+
"exclude": r"^\.venv/",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "skylos",
|
|
90
|
+
"name": "skylos-dead-code-detection",
|
|
91
|
+
"entry": "skylos",
|
|
92
|
+
"language": "system",
|
|
93
|
+
"args": ["crackerjack"],
|
|
94
|
+
"pass_filenames": False,
|
|
95
|
+
"stages": ["pre-push", "manual"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "pyright",
|
|
99
|
+
"name": "pyright-type-checking",
|
|
100
|
+
"entry": "uv run pyright",
|
|
101
|
+
"language": "system",
|
|
102
|
+
"files": r"^crackerjack/.*\.py$",
|
|
103
|
+
"exclude": r"^crackerjack/(mcp|plugins)/.*\.py$|crackerjack/code_cleaner\.py$",
|
|
104
|
+
"stages": ["pre-push", "manual"],
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _build_standard_precommit_hooks(self) -> list[dict[str, t.Any]]:
|
|
109
|
+
"""Build standard pre-commit hook configurations."""
|
|
110
|
+
exclude_pattern = r"^\.venv/"
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
"id": "trailing-whitespace",
|
|
114
|
+
"name": "trailing-whitespace",
|
|
115
|
+
"exclude": exclude_pattern,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "end-of-file-fixer",
|
|
119
|
+
"name": "end-of-file-fixer",
|
|
120
|
+
"exclude": exclude_pattern,
|
|
121
|
+
},
|
|
122
|
+
{"id": "check-yaml", "name": "check-yaml", "exclude": exclude_pattern},
|
|
123
|
+
{"id": "check-toml", "name": "check-toml", "exclude": exclude_pattern},
|
|
124
|
+
{
|
|
125
|
+
"id": "check-added-large-files",
|
|
126
|
+
"name": "check-added-large-files",
|
|
127
|
+
"exclude": exclude_pattern,
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
def _build_pyproject_tools(self) -> dict[str, t.Any]:
|
|
132
|
+
"""Build pyproject.toml tool configurations."""
|
|
133
|
+
return {
|
|
134
|
+
"ruff": self._build_ruff_config(),
|
|
135
|
+
"pytest": self._build_pytest_config(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def _build_ruff_config(self) -> dict[str, t.Any]:
|
|
139
|
+
"""Build Ruff configuration."""
|
|
140
|
+
return {
|
|
141
|
+
"target-version": "py313",
|
|
142
|
+
"line-length": 88,
|
|
143
|
+
"fix": True,
|
|
144
|
+
"unsafe-fixes": True,
|
|
145
|
+
"show-fixes": True,
|
|
146
|
+
"output-format": "full",
|
|
147
|
+
"format": {"docstring-code-format": True},
|
|
148
|
+
"lint": {
|
|
149
|
+
"extend-select": ["C901", "F", "I", "UP"],
|
|
150
|
+
"ignore": ["E402", "F821"],
|
|
151
|
+
"fixable": ["ALL"],
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def _build_pytest_config(self) -> dict[str, t.Any]:
|
|
156
|
+
"""Build pytest configuration."""
|
|
157
|
+
return {
|
|
158
|
+
"ini_options": {
|
|
159
|
+
"asyncio_mode": "auto",
|
|
160
|
+
"timeout": 300,
|
|
161
|
+
"addopts": "--cov=crackerjack --cov-report=term-missing:skip-covered",
|
|
162
|
+
"testpaths": ["tests"],
|
|
163
|
+
"markers": [
|
|
164
|
+
"unit: marks test as a unit test",
|
|
165
|
+
"integration: marks test as an integration test",
|
|
166
|
+
"no_leaks: detect asyncio task leaks",
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def get_template(
|
|
172
|
+
self, config_type: str, version: str | None = None
|
|
173
|
+
) -> ConfigVersion | None:
|
|
174
|
+
"""Get configuration template by type and optional version."""
|
|
175
|
+
if config_type not in self.templates:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
template = self.templates[config_type]
|
|
179
|
+
if version and template.version != version:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return template
|
|
183
|
+
|
|
184
|
+
def check_updates(self, project_path: Path) -> dict[str, ConfigUpdateInfo]:
|
|
185
|
+
"""Check if newer configuration versions are available."""
|
|
186
|
+
updates = {}
|
|
187
|
+
|
|
188
|
+
version_file = project_path / ".crackerjack-config.yaml"
|
|
189
|
+
current_versions = self._load_current_versions(version_file)
|
|
190
|
+
|
|
191
|
+
for config_type, template in self.templates.items():
|
|
192
|
+
current_version = current_versions.get(config_type, "0.0.0")
|
|
193
|
+
needs_update = self._version_compare(current_version, template.version) < 0
|
|
194
|
+
|
|
195
|
+
update_info = ConfigUpdateInfo(
|
|
196
|
+
config_type=config_type,
|
|
197
|
+
current_version=current_version,
|
|
198
|
+
latest_version=template.version,
|
|
199
|
+
needs_update=needs_update,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if needs_update:
|
|
203
|
+
update_info.diff_preview = self._generate_diff_preview(
|
|
204
|
+
config_type, project_path
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
updates[config_type] = update_info
|
|
208
|
+
|
|
209
|
+
return updates
|
|
210
|
+
|
|
211
|
+
def _load_current_versions(self, version_file: Path) -> dict[str, str]:
|
|
212
|
+
"""Load current configuration versions from tracking file."""
|
|
213
|
+
if not version_file.exists():
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
with version_file.open() as f:
|
|
218
|
+
data = yaml.safe_load(f)
|
|
219
|
+
if not isinstance(data, dict):
|
|
220
|
+
return {}
|
|
221
|
+
configs = data.get("configs", {})
|
|
222
|
+
if not isinstance(configs, dict):
|
|
223
|
+
return {}
|
|
224
|
+
return {
|
|
225
|
+
name: config.get("version", "0.0.0")
|
|
226
|
+
for name, config in configs.items()
|
|
227
|
+
if isinstance(config, dict)
|
|
228
|
+
}
|
|
229
|
+
except Exception:
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
def _version_compare(self, version1: str, version2: str) -> int:
|
|
233
|
+
"""Compare two semantic versions. Returns -1, 0, or 1."""
|
|
234
|
+
|
|
235
|
+
def version_tuple(v: str) -> tuple[int, ...]:
|
|
236
|
+
return tuple(int(x) for x in v.split("."))
|
|
237
|
+
|
|
238
|
+
v1_tuple = version_tuple(version1)
|
|
239
|
+
v2_tuple = version_tuple(version2)
|
|
240
|
+
|
|
241
|
+
if v1_tuple < v2_tuple:
|
|
242
|
+
return -1
|
|
243
|
+
elif v1_tuple > v2_tuple:
|
|
244
|
+
return 1
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
def _generate_diff_preview(self, config_type: str, project_path: Path) -> str:
|
|
248
|
+
"""Generate a preview of changes that would be made."""
|
|
249
|
+
if config_type == "pre-commit":
|
|
250
|
+
config_file = project_path / ".pre-commit-config.yaml"
|
|
251
|
+
elif config_type == "pyproject":
|
|
252
|
+
config_file = project_path / "pyproject.toml"
|
|
253
|
+
else:
|
|
254
|
+
return "Diff preview not available for this config type"
|
|
255
|
+
|
|
256
|
+
if not config_file.exists():
|
|
257
|
+
return f"Would create new {config_file.name} file"
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
with config_file.open() as f:
|
|
261
|
+
if config_type == "pre-commit":
|
|
262
|
+
current_config = yaml.safe_load(f)
|
|
263
|
+
else:
|
|
264
|
+
content = f.read()
|
|
265
|
+
current_config = tomli.loads(content)
|
|
266
|
+
|
|
267
|
+
template = self.get_template(config_type)
|
|
268
|
+
if not template:
|
|
269
|
+
return "Template not found"
|
|
270
|
+
|
|
271
|
+
return self._create_config_diff(current_config, template.config_data)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return f"Error generating diff preview: {e}"
|
|
274
|
+
|
|
275
|
+
def _create_config_diff(
|
|
276
|
+
self, current: dict[str, t.Any], new: dict[str, t.Any]
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Create a simple diff between two configurations."""
|
|
279
|
+
changes: list[str] = []
|
|
280
|
+
self._collect_config_changes(current, new, changes)
|
|
281
|
+
|
|
282
|
+
if not changes:
|
|
283
|
+
return "No changes detected"
|
|
284
|
+
|
|
285
|
+
return "\n".join(changes[:10]) # Limit to first 10 changes
|
|
286
|
+
|
|
287
|
+
def _collect_config_changes(
|
|
288
|
+
self,
|
|
289
|
+
current: dict[str, t.Any],
|
|
290
|
+
new: dict[str, t.Any],
|
|
291
|
+
changes: list[str],
|
|
292
|
+
path: str = "",
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Collect configuration changes recursively."""
|
|
295
|
+
self._collect_additions_and_modifications(current, new, changes, path)
|
|
296
|
+
self._collect_removals(current, new, changes, path)
|
|
297
|
+
|
|
298
|
+
def _collect_additions_and_modifications(
|
|
299
|
+
self,
|
|
300
|
+
current: dict[str, t.Any],
|
|
301
|
+
new: dict[str, t.Any],
|
|
302
|
+
changes: list[str],
|
|
303
|
+
path: str,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Collect additions and modifications in configuration."""
|
|
306
|
+
for key, value in new.items():
|
|
307
|
+
key_path = f"{path}.{key}" if path else key
|
|
308
|
+
|
|
309
|
+
if key not in current:
|
|
310
|
+
changes.append(f"+ Add {key_path}: {value}")
|
|
311
|
+
elif self._is_nested_dict(value, current[key]):
|
|
312
|
+
self._collect_config_changes(current[key], value, changes, key_path)
|
|
313
|
+
elif current[key] != value:
|
|
314
|
+
changes.append(f"~ Change {key_path}: {current[key]} → {value}")
|
|
315
|
+
|
|
316
|
+
def _collect_removals(
|
|
317
|
+
self,
|
|
318
|
+
current: dict[str, t.Any],
|
|
319
|
+
new: dict[str, t.Any],
|
|
320
|
+
changes: list[str],
|
|
321
|
+
path: str,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Collect removals in configuration."""
|
|
324
|
+
for key in current:
|
|
325
|
+
if key not in new:
|
|
326
|
+
key_path = f"{path}.{key}" if path else key
|
|
327
|
+
changes.append(f"- Remove {key_path}")
|
|
328
|
+
|
|
329
|
+
def _is_nested_dict(self, new_value: t.Any, current_value: t.Any) -> bool:
|
|
330
|
+
"""Check if both values are dictionaries for nested comparison."""
|
|
331
|
+
return isinstance(new_value, dict) and isinstance(current_value, dict)
|
|
332
|
+
|
|
333
|
+
def apply_update(
|
|
334
|
+
self,
|
|
335
|
+
config_type: str,
|
|
336
|
+
project_path: Path,
|
|
337
|
+
interactive: bool = False,
|
|
338
|
+
) -> bool:
|
|
339
|
+
"""Apply configuration update to project."""
|
|
340
|
+
template = self.get_template(config_type)
|
|
341
|
+
if not template:
|
|
342
|
+
self.console.print(f"[red]❌[/red] Template not found: {config_type}")
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
if config_type == "pre-commit":
|
|
347
|
+
return self._apply_precommit_update(template, project_path, interactive)
|
|
348
|
+
elif config_type == "pyproject":
|
|
349
|
+
return self._apply_pyproject_update(template, project_path, interactive)
|
|
350
|
+
else:
|
|
351
|
+
self.console.print(
|
|
352
|
+
f"[yellow]⚠️[/yellow] Unsupported config type: {config_type}"
|
|
353
|
+
)
|
|
354
|
+
return False
|
|
355
|
+
except Exception as e:
|
|
356
|
+
self.console.print(f"[red]❌[/red] Failed to apply update: {e}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
def _apply_precommit_update(
|
|
360
|
+
self, template: ConfigVersion, project_path: Path, interactive: bool
|
|
361
|
+
) -> bool:
|
|
362
|
+
"""Apply pre-commit configuration update."""
|
|
363
|
+
config_file = project_path / ".pre-commit-config.yaml"
|
|
364
|
+
|
|
365
|
+
if interactive and config_file.exists():
|
|
366
|
+
self.console.print(f"\n[bold cyan]Updating {config_file.name}[/bold cyan]")
|
|
367
|
+
diff = self._generate_diff_preview("pre-commit", project_path)
|
|
368
|
+
self.console.print(f"Changes:\n{diff}")
|
|
369
|
+
|
|
370
|
+
if not self._confirm_update():
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
with config_file.open("w") as f:
|
|
375
|
+
yaml.dump(
|
|
376
|
+
template.config_data, f, default_flow_style=False, sort_keys=False
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
self._update_version_tracking(project_path, "pre-commit", template.version)
|
|
380
|
+
self._invalidate_precommit_cache(project_path)
|
|
381
|
+
|
|
382
|
+
self.console.print(
|
|
383
|
+
f"[green]✅[/green] Updated {config_file.name} to version {template.version}"
|
|
384
|
+
)
|
|
385
|
+
return True
|
|
386
|
+
except Exception as e:
|
|
387
|
+
self.console.print(f"[red]❌[/red] Failed to write config: {e}")
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
def _apply_pyproject_update(
|
|
391
|
+
self, template: ConfigVersion, project_path: Path, interactive: bool
|
|
392
|
+
) -> bool:
|
|
393
|
+
"""Apply pyproject.toml configuration update."""
|
|
394
|
+
config_file = project_path / "pyproject.toml"
|
|
395
|
+
|
|
396
|
+
if not config_file.exists():
|
|
397
|
+
self.console.print(
|
|
398
|
+
f"[yellow]⚠️[/yellow] pyproject.toml not found at {project_path}"
|
|
399
|
+
)
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
if interactive:
|
|
403
|
+
self.console.print(f"\n[bold cyan]Updating {config_file.name}[/bold cyan]")
|
|
404
|
+
diff = self._generate_diff_preview("pyproject", project_path)
|
|
405
|
+
self.console.print(f"Changes:\n{diff}")
|
|
406
|
+
|
|
407
|
+
if not self._confirm_update():
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
# Read existing config
|
|
412
|
+
with config_file.open() as f:
|
|
413
|
+
content = f.read()
|
|
414
|
+
existing_config = tomli.loads(content)
|
|
415
|
+
|
|
416
|
+
# Merge tool sections from template
|
|
417
|
+
if "tool" not in existing_config:
|
|
418
|
+
existing_config["tool"] = {}
|
|
419
|
+
|
|
420
|
+
for tool_name, tool_config in template.config_data.get("tool", {}).items():
|
|
421
|
+
existing_config["tool"][tool_name] = tool_config
|
|
422
|
+
|
|
423
|
+
# Write back using tomli_w
|
|
424
|
+
from tomli_w import dumps
|
|
425
|
+
|
|
426
|
+
updated_content = dumps(existing_config)
|
|
427
|
+
|
|
428
|
+
with config_file.open("w") as f:
|
|
429
|
+
f.write(updated_content)
|
|
430
|
+
|
|
431
|
+
self._update_version_tracking(project_path, "pyproject", template.version)
|
|
432
|
+
|
|
433
|
+
self.console.print(
|
|
434
|
+
f"[green]✅[/green] Updated {config_file.name} to version {template.version}"
|
|
435
|
+
)
|
|
436
|
+
return True
|
|
437
|
+
except Exception as e:
|
|
438
|
+
self.console.print(f"[red]❌[/red] Failed to update pyproject.toml: {e}")
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
def _confirm_update(self) -> bool:
|
|
442
|
+
"""Ask user to confirm update."""
|
|
443
|
+
try:
|
|
444
|
+
response = input("\nApply this update? [y/N]: ").strip().lower()
|
|
445
|
+
return response in ("y", "yes")
|
|
446
|
+
except (EOFError, KeyboardInterrupt):
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
def _update_version_tracking(
|
|
450
|
+
self, project_path: Path, config_type: str, version: str
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Update version tracking file."""
|
|
453
|
+
version_file = project_path / ".crackerjack-config.yaml"
|
|
454
|
+
|
|
455
|
+
data = {"version": "1.0.0", "configs": {}}
|
|
456
|
+
if version_file.exists():
|
|
457
|
+
with suppress(Exception):
|
|
458
|
+
with version_file.open() as f:
|
|
459
|
+
existing_data = yaml.safe_load(f)
|
|
460
|
+
if isinstance(existing_data, dict):
|
|
461
|
+
data = existing_data
|
|
462
|
+
|
|
463
|
+
if "configs" not in data:
|
|
464
|
+
data["configs"] = {}
|
|
465
|
+
|
|
466
|
+
data["configs"][config_type] = {
|
|
467
|
+
"version": version,
|
|
468
|
+
"last_updated": datetime.now().isoformat(),
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
with suppress(Exception):
|
|
472
|
+
with version_file.open("w") as f:
|
|
473
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
474
|
+
|
|
475
|
+
def _invalidate_precommit_cache(self, project_path: Path) -> None:
|
|
476
|
+
"""Invalidate pre-commit cache to ensure fresh environment."""
|
|
477
|
+
with suppress(Exception):
|
|
478
|
+
subprocess.run(
|
|
479
|
+
["pre-commit", "clean"],
|
|
480
|
+
cwd=project_path,
|
|
481
|
+
capture_output=True,
|
|
482
|
+
text=True,
|
|
483
|
+
timeout=30,
|
|
484
|
+
)
|
|
485
|
+
subprocess.run(
|
|
486
|
+
["pre-commit", "install"],
|
|
487
|
+
cwd=project_path,
|
|
488
|
+
capture_output=True,
|
|
489
|
+
text=True,
|
|
490
|
+
timeout=30,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def get_config_hash(self, config_path: Path) -> str:
|
|
494
|
+
"""Generate hash of configuration file for cache invalidation."""
|
|
495
|
+
if not config_path.exists():
|
|
496
|
+
return ""
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
content = config_path.read_text()
|
|
500
|
+
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
501
|
+
except Exception:
|
|
502
|
+
return ""
|
|
503
|
+
|
|
504
|
+
def list_available_templates(self) -> dict[str, str]:
|
|
505
|
+
"""List all available configuration templates."""
|
|
506
|
+
return {name: template.description for name, template in self.templates.items()}
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import time
|
|
4
4
|
import tomllib
|
|
5
|
+
import typing as t
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -127,7 +128,7 @@ class ContextualAIAssistant:
|
|
|
127
128
|
category="testing",
|
|
128
129
|
priority="medium",
|
|
129
130
|
title="Progress Toward 100 % Coverage",
|
|
130
|
-
description=f"Current coverage: {context.test_coverage
|
|
131
|
+
description=f"Current coverage: {context.test_coverage:.1f}%. Next milestone: {next_milestone}% on the journey to 100%.",
|
|
131
132
|
action_command="python -m crackerjack -t",
|
|
132
133
|
reasoning="Coverage ratchet system prevents regression and targets 100 % coverage incrementally",
|
|
133
134
|
confidence=0.85,
|
|
@@ -149,7 +150,7 @@ class ContextualAIAssistant:
|
|
|
149
150
|
priority="high",
|
|
150
151
|
title="Fix Lint Errors",
|
|
151
152
|
description=f"Found {context.lint_errors_count} lint errors that should be addressed.",
|
|
152
|
-
action_command="python -m crackerjack --ai-
|
|
153
|
+
action_command="python -m crackerjack --ai-fix",
|
|
153
154
|
reasoning="High lint error count indicates technical debt and potential bugs",
|
|
154
155
|
confidence=0.95,
|
|
155
156
|
),
|
|
@@ -297,7 +298,7 @@ class ContextualAIAssistant:
|
|
|
297
298
|
|
|
298
299
|
def _determine_project_size(self) -> str:
|
|
299
300
|
try:
|
|
300
|
-
python_files = list(self.project_root.rglob("*.py"))
|
|
301
|
+
python_files = list[t.Any](self.project_root.rglob("*.py"))
|
|
301
302
|
if len(python_files) < 10:
|
|
302
303
|
return "small"
|
|
303
304
|
if len(python_files) < 50:
|
|
@@ -481,34 +482,59 @@ class ContextualAIAssistant:
|
|
|
481
482
|
10 - int(rec.confidence * 10)
|
|
482
483
|
)
|
|
483
484
|
self.console.print(
|
|
484
|
-
f" [dim]Confidence: [{confidence_bar}] {rec.confidence
|
|
485
|
+
f" [dim]Confidence: [{confidence_bar}] {rec.confidence:.1%}[/ dim]",
|
|
485
486
|
)
|
|
486
487
|
|
|
487
488
|
if i < len(recommendations):
|
|
488
489
|
self.console.print()
|
|
489
490
|
|
|
490
491
|
def get_quick_help(self, query: str) -> str:
|
|
492
|
+
"""Get quick help for common queries using keyword matching."""
|
|
491
493
|
query_lower = query.lower()
|
|
492
494
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
+
# Define help responses with keywords
|
|
496
|
+
help_mapping = self._get_help_keyword_mapping()
|
|
495
497
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
+
# Find the first matching help response
|
|
499
|
+
for keywords, response in help_mapping:
|
|
500
|
+
if self._query_contains_keywords(query_lower, keywords):
|
|
501
|
+
return response
|
|
498
502
|
|
|
499
|
-
|
|
500
|
-
return "Fix code style with: python -m crackerjack\nFor AI-powered fixes: python -m crackerjack --ai-agent"
|
|
503
|
+
return "For full help, run: python -m crackerjack --help\nFor AI assistance: python -m crackerjack --ai-fix"
|
|
501
504
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
505
|
+
def _get_help_keyword_mapping(self) -> list[tuple[list[str], str]]:
|
|
506
|
+
"""Get mapping of keywords to help responses."""
|
|
507
|
+
return [
|
|
508
|
+
(
|
|
509
|
+
["coverage"],
|
|
510
|
+
"Check test coverage with: python -m crackerjack -t\nView HTML report: uv run coverage html",
|
|
511
|
+
),
|
|
512
|
+
(
|
|
513
|
+
["security", "vulnerabilit"],
|
|
514
|
+
"Check security with: python -m crackerjack --check-dependencies\nRun security audit: uv run bandit -r .",
|
|
515
|
+
),
|
|
516
|
+
(
|
|
517
|
+
["lint", "format"],
|
|
518
|
+
"Fix code style with: python -m crackerjack\nFor AI-powered fixes: python -m crackerjack --ai-fix",
|
|
519
|
+
),
|
|
520
|
+
(
|
|
521
|
+
["test"],
|
|
522
|
+
"Run tests with: python -m crackerjack -t\nFor AI-powered test fixes: python -m crackerjack --ai-fix -t",
|
|
523
|
+
),
|
|
524
|
+
(
|
|
525
|
+
["publish", "release"],
|
|
526
|
+
"Publish to PyPI: python -m crackerjack -p patch\nBump version only: python -m crackerjack -b patch",
|
|
527
|
+
),
|
|
528
|
+
(
|
|
529
|
+
["clean"],
|
|
530
|
+
"Clean code: python -m crackerjack -x\nNote: Resolve TODOs first before cleaning",
|
|
531
|
+
),
|
|
532
|
+
(
|
|
533
|
+
["dashboard", "monitor"],
|
|
534
|
+
"Start monitoring dashboard: python -m crackerjack --dashboard\nStart WebSocket server: python -m crackerjack --start-websocket-server",
|
|
535
|
+
),
|
|
536
|
+
]
|
|
513
537
|
|
|
514
|
-
|
|
538
|
+
def _query_contains_keywords(self, query: str, keywords: list[str]) -> bool:
|
|
539
|
+
"""Check if query contains any of the specified keywords."""
|
|
540
|
+
return any(keyword in query for keyword in keywords)
|