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
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import typing as t
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from crackerjack.errors import ValidationError
|
|
13
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
14
|
+
from crackerjack.services.logging import LoggingContext, get_logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CrackerjackConfig(BaseModel):
|
|
18
|
+
package_path: Path = Field(default_factory=Path.cwd)
|
|
19
|
+
cache_enabled: bool = True
|
|
20
|
+
cache_size: int = 1000
|
|
21
|
+
cache_ttl: float = 300.0
|
|
22
|
+
|
|
23
|
+
hook_batch_size: int = 10
|
|
24
|
+
hook_timeout: int = 300
|
|
25
|
+
max_concurrent_hooks: int = 4
|
|
26
|
+
enable_async_hooks: bool = True
|
|
27
|
+
|
|
28
|
+
test_timeout: int = 300
|
|
29
|
+
test_workers: int = Field(default_factory=lambda: os.cpu_count() or 1)
|
|
30
|
+
min_coverage: float = 10.11 # Baseline from coverage ratchet system
|
|
31
|
+
|
|
32
|
+
log_level: str = "INFO"
|
|
33
|
+
log_json: bool = False
|
|
34
|
+
log_file: Path | None = None
|
|
35
|
+
enable_correlation_ids: bool = True
|
|
36
|
+
|
|
37
|
+
autofix: bool = True
|
|
38
|
+
skip_hooks: bool = False
|
|
39
|
+
experimental_hooks: bool = False
|
|
40
|
+
|
|
41
|
+
performance_tracking: bool = True
|
|
42
|
+
benchmark_mode: bool = False
|
|
43
|
+
|
|
44
|
+
publish_enabled: bool = False
|
|
45
|
+
keyring_provider: str = "subprocess"
|
|
46
|
+
|
|
47
|
+
batch_file_operations: bool = True
|
|
48
|
+
file_operation_batch_size: int = 10
|
|
49
|
+
|
|
50
|
+
precommit_mode: str = "comprehensive"
|
|
51
|
+
|
|
52
|
+
@field_validator("package_path", mode="before")
|
|
53
|
+
@classmethod
|
|
54
|
+
def validate_package_path(cls, v: Any) -> Path:
|
|
55
|
+
if isinstance(v, str):
|
|
56
|
+
v = Path(v)
|
|
57
|
+
return v.resolve()
|
|
58
|
+
|
|
59
|
+
@field_validator("log_file", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def validate_log_file(cls, v: Any) -> Path | None:
|
|
62
|
+
if v is None:
|
|
63
|
+
return v
|
|
64
|
+
if isinstance(v, str):
|
|
65
|
+
v = Path(v)
|
|
66
|
+
return v
|
|
67
|
+
|
|
68
|
+
@field_validator("test_workers")
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_test_workers(cls, v: int) -> int:
|
|
71
|
+
return max(1, min(v, 16))
|
|
72
|
+
|
|
73
|
+
@field_validator("min_coverage")
|
|
74
|
+
@classmethod
|
|
75
|
+
def validate_min_coverage(cls, v: float) -> float:
|
|
76
|
+
return max(0.0, min(v, 100.0))
|
|
77
|
+
|
|
78
|
+
@field_validator("log_level")
|
|
79
|
+
@classmethod
|
|
80
|
+
def validate_log_level(cls, v: str) -> str:
|
|
81
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
82
|
+
if v.upper() not in valid_levels:
|
|
83
|
+
msg = f"Invalid log level: {v}. Must be one of {valid_levels}"
|
|
84
|
+
raise ValueError(msg)
|
|
85
|
+
return v.upper()
|
|
86
|
+
|
|
87
|
+
class Config:
|
|
88
|
+
extra = "allow"
|
|
89
|
+
|
|
90
|
+
use_enum_values = True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ConfigSource:
|
|
94
|
+
def __init__(self, priority: int = 0) -> None:
|
|
95
|
+
self.priority = priority
|
|
96
|
+
self.logger = get_logger("crackerjack.config.source")
|
|
97
|
+
|
|
98
|
+
def load(self) -> dict[str, Any]:
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
def is_available(self) -> bool:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class EnvironmentConfigSource(ConfigSource):
|
|
106
|
+
ENV_PREFIX = "CRACKERJACK_"
|
|
107
|
+
|
|
108
|
+
def __init__(self, priority: int = 100) -> None:
|
|
109
|
+
super().__init__(priority)
|
|
110
|
+
|
|
111
|
+
def load(self) -> dict[str, Any]:
|
|
112
|
+
config: dict[str, Any] = {}
|
|
113
|
+
|
|
114
|
+
for key, value in os.environ.items():
|
|
115
|
+
if key.startswith(self.ENV_PREFIX):
|
|
116
|
+
config_key = key[len(self.ENV_PREFIX) :].lower()
|
|
117
|
+
|
|
118
|
+
config[config_key] = self._convert_value(value)
|
|
119
|
+
|
|
120
|
+
self.logger.debug("Loaded environment config", keys=list(config.keys()))
|
|
121
|
+
return config
|
|
122
|
+
|
|
123
|
+
def _convert_value(self, value: str) -> Any:
|
|
124
|
+
if value.lower() in ("true", "1", "yes", "on"):
|
|
125
|
+
return True
|
|
126
|
+
if value.lower() in ("false", "0", "no", "off"):
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
with suppress(ValueError):
|
|
130
|
+
return int(value)
|
|
131
|
+
|
|
132
|
+
with suppress(ValueError):
|
|
133
|
+
return float(value)
|
|
134
|
+
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class FileConfigSource(ConfigSource):
|
|
139
|
+
def __init__(self, file_path: Path, priority: int = 50) -> None:
|
|
140
|
+
super().__init__(priority)
|
|
141
|
+
self.file_path = file_path
|
|
142
|
+
|
|
143
|
+
def is_available(self) -> bool:
|
|
144
|
+
return self.file_path.exists() and self.file_path.is_file()
|
|
145
|
+
|
|
146
|
+
def load(self) -> dict[str, Any]:
|
|
147
|
+
if not self.is_available():
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
content = self.file_path.read_text()
|
|
152
|
+
|
|
153
|
+
if self.file_path.suffix.lower() in (".yml", ".yaml"):
|
|
154
|
+
yaml_result: t.Any = yaml.safe_load(content)
|
|
155
|
+
config = (
|
|
156
|
+
t.cast("dict[str, Any]", yaml_result)
|
|
157
|
+
if yaml_result is not None
|
|
158
|
+
else {}
|
|
159
|
+
)
|
|
160
|
+
elif self.file_path.suffix.lower() == ".json":
|
|
161
|
+
json_result = json.loads(content)
|
|
162
|
+
config = (
|
|
163
|
+
t.cast("dict[str, Any]", json_result)
|
|
164
|
+
if json_result is not None
|
|
165
|
+
else {}
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
self.logger.warning(
|
|
169
|
+
"Unknown config file format",
|
|
170
|
+
path=str(self.file_path),
|
|
171
|
+
)
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
self.logger.debug(
|
|
175
|
+
"Loaded file config",
|
|
176
|
+
path=str(self.file_path),
|
|
177
|
+
keys=list(config.keys()),
|
|
178
|
+
)
|
|
179
|
+
return config
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.logger.exception(
|
|
183
|
+
"Failed to load config file",
|
|
184
|
+
path=str(self.file_path),
|
|
185
|
+
error=str(e),
|
|
186
|
+
)
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class PyprojectConfigSource(ConfigSource):
|
|
191
|
+
def __init__(self, pyproject_path: Path, priority: int = 25) -> None:
|
|
192
|
+
super().__init__(priority)
|
|
193
|
+
self.pyproject_path = pyproject_path
|
|
194
|
+
|
|
195
|
+
def is_available(self) -> bool:
|
|
196
|
+
return self.pyproject_path.exists() and self.pyproject_path.is_file()
|
|
197
|
+
|
|
198
|
+
def load(self) -> dict[str, Any]:
|
|
199
|
+
if not self.is_available():
|
|
200
|
+
return {}
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
import tomllib
|
|
204
|
+
|
|
205
|
+
with self.pyproject_path.open("rb") as f:
|
|
206
|
+
pyproject_data = tomllib.load(f)
|
|
207
|
+
|
|
208
|
+
config = pyproject_data.get("tool", {}).get("crackerjack", {})
|
|
209
|
+
|
|
210
|
+
self.logger.debug("Loaded pyproject config", keys=list(config.keys()))
|
|
211
|
+
return config
|
|
212
|
+
|
|
213
|
+
except ImportError:
|
|
214
|
+
try:
|
|
215
|
+
import tomllib
|
|
216
|
+
|
|
217
|
+
with self.pyproject_path.open("rb") as f:
|
|
218
|
+
pyproject_data = tomllib.load(f)
|
|
219
|
+
config = pyproject_data.get("tool", {}).get("crackerjack", {})
|
|
220
|
+
self.logger.debug("Loaded pyproject config", keys=list(config.keys()))
|
|
221
|
+
return config
|
|
222
|
+
except ImportError:
|
|
223
|
+
self.logger.warning(
|
|
224
|
+
"No TOML library available for pyproject.toml parsing",
|
|
225
|
+
)
|
|
226
|
+
return {}
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self.logger.exception("Failed to load pyproject.toml", error=str(e))
|
|
229
|
+
return {}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class OptionsConfigSource(ConfigSource):
|
|
233
|
+
def __init__(self, options: OptionsProtocol, priority: int = 200) -> None:
|
|
234
|
+
super().__init__(priority)
|
|
235
|
+
self.options = options
|
|
236
|
+
|
|
237
|
+
def load(self) -> dict[str, Any]:
|
|
238
|
+
config: dict[str, Any] = {}
|
|
239
|
+
|
|
240
|
+
option_mappings = {
|
|
241
|
+
"testing": "test_mode",
|
|
242
|
+
"autofix": "autofix",
|
|
243
|
+
"skip_hooks": "skip_hooks",
|
|
244
|
+
"experimental_hooks": "experimental_hooks",
|
|
245
|
+
"test_timeout": "test_timeout",
|
|
246
|
+
"test_workers": "test_workers",
|
|
247
|
+
"benchmark": "benchmark_mode",
|
|
248
|
+
"publish": "publish_enabled",
|
|
249
|
+
"log_level": "log_level",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for option_attr, config_key in option_mappings.items():
|
|
253
|
+
if hasattr(self.options, option_attr):
|
|
254
|
+
value = getattr(self.options, option_attr)
|
|
255
|
+
if value is not None:
|
|
256
|
+
config[config_key] = value
|
|
257
|
+
|
|
258
|
+
self.logger.debug("Loaded options config", keys=list(config.keys()))
|
|
259
|
+
return config
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class UnifiedConfigurationService:
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
console: Console,
|
|
266
|
+
pkg_path: Path,
|
|
267
|
+
options: OptionsProtocol | None = None,
|
|
268
|
+
) -> None:
|
|
269
|
+
self.console = console
|
|
270
|
+
self.pkg_path = pkg_path
|
|
271
|
+
self.logger = get_logger("crackerjack.config.unified")
|
|
272
|
+
|
|
273
|
+
self.sources: list[ConfigSource] = []
|
|
274
|
+
|
|
275
|
+
pyproject_path = pkg_path / "pyproject.toml"
|
|
276
|
+
self.sources.extend(
|
|
277
|
+
(
|
|
278
|
+
self._create_default_source(),
|
|
279
|
+
PyprojectConfigSource(pyproject_path),
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# .crackerjack.* config files are no longer supported
|
|
284
|
+
# Configuration should be done through pyproject.toml
|
|
285
|
+
|
|
286
|
+
self.sources.append(EnvironmentConfigSource())
|
|
287
|
+
|
|
288
|
+
if options:
|
|
289
|
+
self.sources.append(OptionsConfigSource(options))
|
|
290
|
+
|
|
291
|
+
self._config: CrackerjackConfig | None = None
|
|
292
|
+
|
|
293
|
+
def _create_default_source(self) -> ConfigSource:
|
|
294
|
+
pkg_path = self.pkg_path
|
|
295
|
+
|
|
296
|
+
class DefaultConfigSource(ConfigSource):
|
|
297
|
+
def load(self) -> dict[str, Any]:
|
|
298
|
+
return {
|
|
299
|
+
"package_path": pkg_path,
|
|
300
|
+
"cache_enabled": True,
|
|
301
|
+
"autofix": True,
|
|
302
|
+
"log_level": "INFO",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return DefaultConfigSource(priority=0)
|
|
306
|
+
|
|
307
|
+
def get_config(self, reload: bool = False) -> CrackerjackConfig:
|
|
308
|
+
if self._config is None or reload:
|
|
309
|
+
with LoggingContext("load_unified_config", source_count=len(self.sources)):
|
|
310
|
+
self._config = self._load_unified_config()
|
|
311
|
+
|
|
312
|
+
return self._config
|
|
313
|
+
|
|
314
|
+
def _load_unified_config(self) -> CrackerjackConfig:
|
|
315
|
+
merged_config: dict[str, Any] = {}
|
|
316
|
+
|
|
317
|
+
sorted_sources = sorted(self.sources, key=lambda s: s.priority)
|
|
318
|
+
|
|
319
|
+
for source in sorted_sources:
|
|
320
|
+
if source.is_available():
|
|
321
|
+
try:
|
|
322
|
+
source_config = source.load()
|
|
323
|
+
if source_config:
|
|
324
|
+
merged_config.update(source_config)
|
|
325
|
+
self.logger.debug(
|
|
326
|
+
"Merged config from source",
|
|
327
|
+
source_type=type(source).__name__,
|
|
328
|
+
priority=source.priority,
|
|
329
|
+
keys=list(source_config.keys()),
|
|
330
|
+
)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self.logger.exception(
|
|
333
|
+
"Failed to load config from source",
|
|
334
|
+
source_type=type(source).__name__,
|
|
335
|
+
error=str(e),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
config = CrackerjackConfig(**merged_config)
|
|
340
|
+
|
|
341
|
+
self.logger.info(
|
|
342
|
+
"Unified configuration loaded",
|
|
343
|
+
package_path=str(config.package_path),
|
|
344
|
+
cache_enabled=config.cache_enabled,
|
|
345
|
+
autofix=config.autofix,
|
|
346
|
+
async_hooks=config.enable_async_hooks,
|
|
347
|
+
test_workers=config.test_workers,
|
|
348
|
+
log_level=config.log_level,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return config
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
self.logger.exception("Configuration validation failed", error=str(e))
|
|
355
|
+
raise ValidationError(
|
|
356
|
+
message="Invalid configuration",
|
|
357
|
+
details=str(e),
|
|
358
|
+
recovery="Check configuration files and environment variables",
|
|
359
|
+
) from e
|
|
360
|
+
|
|
361
|
+
def get_precommit_config_mode(self) -> str:
|
|
362
|
+
config = self.get_config()
|
|
363
|
+
|
|
364
|
+
if config.experimental_hooks:
|
|
365
|
+
return "experimental"
|
|
366
|
+
if hasattr(config, "test") and getattr(config, "test", False):
|
|
367
|
+
return "comprehensive"
|
|
368
|
+
return config.precommit_mode
|
|
369
|
+
|
|
370
|
+
def get_logging_config(self) -> dict[str, Any]:
|
|
371
|
+
config = self.get_config()
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
"level": config.log_level,
|
|
375
|
+
"json_output": config.log_json,
|
|
376
|
+
"log_file": config.log_file,
|
|
377
|
+
"enable_correlation_ids": config.enable_correlation_ids,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
def get_hook_execution_config(self) -> dict[str, Any]:
|
|
381
|
+
config = self.get_config()
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"batch_size": config.hook_batch_size,
|
|
385
|
+
"timeout": config.hook_timeout,
|
|
386
|
+
"max_concurrent": config.max_concurrent_hooks,
|
|
387
|
+
"enable_async": config.enable_async_hooks,
|
|
388
|
+
"autofix": config.autofix,
|
|
389
|
+
"skip_hooks": config.skip_hooks,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def get_testing_config(self) -> dict[str, Any]:
|
|
393
|
+
config = self.get_config()
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"timeout": config.test_timeout,
|
|
397
|
+
"workers": config.test_workers,
|
|
398
|
+
"min_coverage": config.min_coverage,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
def get_cache_config(self) -> dict[str, Any]:
|
|
402
|
+
config = self.get_config()
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
"enabled": config.cache_enabled,
|
|
406
|
+
"size": config.cache_size,
|
|
407
|
+
"ttl": config.cache_ttl,
|
|
408
|
+
"batch_operations": config.batch_file_operations,
|
|
409
|
+
"batch_size": config.file_operation_batch_size,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
def validate_current_config(self) -> bool:
|
|
413
|
+
try:
|
|
414
|
+
config = self.get_config()
|
|
415
|
+
|
|
416
|
+
validation_errors: list[str] = []
|
|
417
|
+
|
|
418
|
+
if config.test_workers <= 0:
|
|
419
|
+
validation_errors.append("test_workers must be positive")
|
|
420
|
+
|
|
421
|
+
if config.min_coverage < 0 or config.min_coverage > 100:
|
|
422
|
+
validation_errors.append("min_coverage must be between 0 and 100")
|
|
423
|
+
|
|
424
|
+
if config.cache_size <= 0:
|
|
425
|
+
validation_errors.append("cache_size must be positive")
|
|
426
|
+
|
|
427
|
+
if validation_errors:
|
|
428
|
+
for error in validation_errors:
|
|
429
|
+
self.logger.error("Configuration validation error", error=error)
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
self.logger.info("Configuration validation passed")
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
except Exception as e:
|
|
436
|
+
self.logger.exception("Configuration validation failed", error=str(e))
|
|
437
|
+
return False
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Core version checking and comparison functionality.
|
|
2
|
+
|
|
3
|
+
This module handles tool version detection, comparison, and update notifications.
|
|
4
|
+
Split from tool_version_service.py to follow single responsibility principle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import typing as t
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class VersionInfo:
|
|
17
|
+
"""Information about a tool's version and update status."""
|
|
18
|
+
|
|
19
|
+
tool_name: str
|
|
20
|
+
current_version: str
|
|
21
|
+
latest_version: str | None = None
|
|
22
|
+
update_available: bool = False
|
|
23
|
+
error: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VersionChecker:
|
|
27
|
+
"""Service for checking tool versions and updates."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, console: Console) -> None:
|
|
30
|
+
self.console = console
|
|
31
|
+
self.tools_to_check = {
|
|
32
|
+
"ruff": self._get_ruff_version,
|
|
33
|
+
"pyright": self._get_pyright_version,
|
|
34
|
+
"pre-commit": self._get_precommit_version,
|
|
35
|
+
"uv": self._get_uv_version,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async def check_tool_updates(self) -> dict[str, VersionInfo]:
|
|
39
|
+
"""Check updates for all registered tools."""
|
|
40
|
+
results = {}
|
|
41
|
+
for tool_name, version_getter in self.tools_to_check.items():
|
|
42
|
+
results[tool_name] = await self._check_single_tool(
|
|
43
|
+
tool_name, version_getter
|
|
44
|
+
)
|
|
45
|
+
return results
|
|
46
|
+
|
|
47
|
+
async def _check_single_tool(
|
|
48
|
+
self, tool_name: str, version_getter: t.Callable[[], str | None]
|
|
49
|
+
) -> VersionInfo:
|
|
50
|
+
"""Check updates for a single tool."""
|
|
51
|
+
try:
|
|
52
|
+
current_version = version_getter()
|
|
53
|
+
if current_version:
|
|
54
|
+
latest_version = await self._fetch_latest_version(tool_name)
|
|
55
|
+
return self._create_installed_version_info(
|
|
56
|
+
tool_name, current_version, latest_version
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
return self._create_missing_tool_info(tool_name)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return self._create_error_version_info(tool_name, e)
|
|
62
|
+
|
|
63
|
+
def _create_installed_version_info(
|
|
64
|
+
self, tool_name: str, current_version: str, latest_version: str | None
|
|
65
|
+
) -> VersionInfo:
|
|
66
|
+
"""Create version info for installed tool."""
|
|
67
|
+
update_available = (
|
|
68
|
+
latest_version is not None
|
|
69
|
+
and self._version_compare(current_version, latest_version) < 0
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if update_available:
|
|
73
|
+
self.console.print(
|
|
74
|
+
f"[yellow]🔄 {tool_name} update available: "
|
|
75
|
+
f"{current_version} → {latest_version}[/yellow]"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return VersionInfo(
|
|
79
|
+
tool_name=tool_name,
|
|
80
|
+
current_version=current_version,
|
|
81
|
+
latest_version=latest_version,
|
|
82
|
+
update_available=update_available,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _create_missing_tool_info(self, tool_name: str) -> VersionInfo:
|
|
86
|
+
"""Create version info for missing tool."""
|
|
87
|
+
self.console.print(f"[red]⚠️ {tool_name} not installed[/red]")
|
|
88
|
+
return VersionInfo(
|
|
89
|
+
tool_name=tool_name,
|
|
90
|
+
current_version="not installed",
|
|
91
|
+
error=f"{tool_name} not found or not installed",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _create_error_version_info(
|
|
95
|
+
self, tool_name: str, error: Exception
|
|
96
|
+
) -> VersionInfo:
|
|
97
|
+
"""Create version info for tool with error."""
|
|
98
|
+
self.console.print(f"[red]❌ Error checking {tool_name}: {error}[/red]")
|
|
99
|
+
return VersionInfo(
|
|
100
|
+
tool_name=tool_name,
|
|
101
|
+
current_version="unknown",
|
|
102
|
+
error=str(error),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _get_ruff_version(self) -> str | None:
|
|
106
|
+
"""Get currently installed Ruff version."""
|
|
107
|
+
return self._get_tool_version("ruff")
|
|
108
|
+
|
|
109
|
+
def _get_pyright_version(self) -> str | None:
|
|
110
|
+
"""Get currently installed Pyright version."""
|
|
111
|
+
return self._get_tool_version("pyright")
|
|
112
|
+
|
|
113
|
+
def _get_precommit_version(self) -> str | None:
|
|
114
|
+
"""Get currently installed pre-commit version."""
|
|
115
|
+
return self._get_tool_version("pre-commit")
|
|
116
|
+
|
|
117
|
+
def _get_uv_version(self) -> str | None:
|
|
118
|
+
"""Get currently installed UV version."""
|
|
119
|
+
return self._get_tool_version("uv")
|
|
120
|
+
|
|
121
|
+
def _get_tool_version(self, tool_name: str) -> str | None:
|
|
122
|
+
"""Generic method to get tool version via subprocess."""
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
[tool_name, "--version"],
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
timeout=10,
|
|
129
|
+
check=False,
|
|
130
|
+
)
|
|
131
|
+
if result.returncode == 0:
|
|
132
|
+
version_line = result.stdout.strip()
|
|
133
|
+
return version_line.split()[-1] if version_line else None
|
|
134
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
135
|
+
pass
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
async def _fetch_latest_version(self, tool_name: str) -> str | None:
|
|
139
|
+
"""Fetch latest version from PyPI."""
|
|
140
|
+
try:
|
|
141
|
+
pypi_urls = {
|
|
142
|
+
"ruff": "https://pypi.org/pypi/ruff/json",
|
|
143
|
+
"pyright": "https://pypi.org/pypi/pyright/json",
|
|
144
|
+
"pre-commit": "https://pypi.org/pypi/pre-commit/json",
|
|
145
|
+
"uv": "https://pypi.org/pypi/uv/json",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
url = pypi_urls.get(tool_name)
|
|
149
|
+
if not url:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
timeout = aiohttp.ClientTimeout(total=10.0)
|
|
153
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
154
|
+
async with session.get(url) as response:
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
data = await response.json()
|
|
157
|
+
return data.get("info", {}).get("version")
|
|
158
|
+
|
|
159
|
+
except Exception:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def _version_compare(self, current: str, latest: str) -> int:
|
|
163
|
+
"""Compare two version strings. Returns -1 if current < latest, 0 if equal, 1 if current > latest."""
|
|
164
|
+
try:
|
|
165
|
+
current_parts, current_len = self._parse_version_parts(current)
|
|
166
|
+
latest_parts, latest_len = self._parse_version_parts(latest)
|
|
167
|
+
|
|
168
|
+
# Normalize lengths
|
|
169
|
+
normalized_current, normalized_latest = self._normalize_version_parts(
|
|
170
|
+
current_parts, latest_parts
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Compare numeric values
|
|
174
|
+
numeric_result = self._compare_numeric_parts(
|
|
175
|
+
normalized_current, normalized_latest
|
|
176
|
+
)
|
|
177
|
+
if numeric_result != 0:
|
|
178
|
+
return numeric_result
|
|
179
|
+
|
|
180
|
+
# Handle length differences when numeric values are equal
|
|
181
|
+
return self._handle_length_differences(
|
|
182
|
+
current_len, latest_len, normalized_current, normalized_latest
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except (ValueError, AttributeError):
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
def _parse_version_parts(self, version: str) -> tuple[list[int], int]:
|
|
189
|
+
"""Parse version string into integer parts and return original length."""
|
|
190
|
+
parts = [int(x) for x in version.split(".")]
|
|
191
|
+
return parts, len(parts)
|
|
192
|
+
|
|
193
|
+
def _normalize_version_parts(
|
|
194
|
+
self, current_parts: list[int], latest_parts: list[int]
|
|
195
|
+
) -> tuple[list[int], list[int]]:
|
|
196
|
+
"""Extend version parts to same length with zeros."""
|
|
197
|
+
max_len = max(len(current_parts), len(latest_parts))
|
|
198
|
+
current_normalized = current_parts + [0] * (max_len - len(current_parts))
|
|
199
|
+
latest_normalized = latest_parts + [0] * (max_len - len(latest_parts))
|
|
200
|
+
return current_normalized, latest_normalized
|
|
201
|
+
|
|
202
|
+
def _compare_numeric_parts(
|
|
203
|
+
self, current_parts: list[int], latest_parts: list[int]
|
|
204
|
+
) -> int:
|
|
205
|
+
"""Compare version parts numerically."""
|
|
206
|
+
for current_part, latest_part in zip(current_parts, latest_parts):
|
|
207
|
+
if current_part < latest_part:
|
|
208
|
+
return -1
|
|
209
|
+
if current_part > latest_part:
|
|
210
|
+
return 1
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
def _handle_length_differences(
|
|
214
|
+
self,
|
|
215
|
+
current_len: int,
|
|
216
|
+
latest_len: int,
|
|
217
|
+
current_parts: list[int],
|
|
218
|
+
latest_parts: list[int],
|
|
219
|
+
) -> int:
|
|
220
|
+
"""Handle version comparison when lengths differ but numeric values are equal."""
|
|
221
|
+
if current_len == latest_len:
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
if current_len < latest_len:
|
|
225
|
+
return self._compare_when_current_shorter(
|
|
226
|
+
current_len, latest_len, latest_parts
|
|
227
|
+
)
|
|
228
|
+
return self._compare_when_latest_shorter(latest_len, current_len, current_parts)
|
|
229
|
+
|
|
230
|
+
def _compare_when_current_shorter(
|
|
231
|
+
self, current_len: int, latest_len: int, latest_parts: list[int]
|
|
232
|
+
) -> int:
|
|
233
|
+
"""Compare when current version has fewer parts than latest."""
|
|
234
|
+
extra_parts = latest_parts[current_len:]
|
|
235
|
+
if any(part != 0 for part in extra_parts):
|
|
236
|
+
return -1
|
|
237
|
+
# "1.0" vs "1.0.0" should return -1, but "1" vs "1.0" should return 0
|
|
238
|
+
return -1 if current_len > 1 else 0
|
|
239
|
+
|
|
240
|
+
def _compare_when_latest_shorter(
|
|
241
|
+
self, latest_len: int, current_len: int, current_parts: list[int]
|
|
242
|
+
) -> int:
|
|
243
|
+
"""Compare when latest version has fewer parts than current."""
|
|
244
|
+
extra_parts = current_parts[latest_len:]
|
|
245
|
+
if any(part != 0 for part in extra_parts):
|
|
246
|
+
return 1
|
|
247
|
+
# "1.0.0" vs "1.0" should return 1, but "1.0" vs "1" should return 0
|
|
248
|
+
return 1 if latest_len > 1 else 0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
SLASH_COMMANDS_DIR = Path(__file__).parent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_slash_command_path(command_name: str) -> Path:
|
|
7
|
+
return SLASH_COMMANDS_DIR / f"{command_name}.md"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_available_commands() -> list[str]:
|
|
11
|
+
return [f.stem for f in SLASH_COMMANDS_DIR.glob("*.md")]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["SLASH_COMMANDS_DIR", "get_slash_command_path", "list_available_commands"]
|