crackerjack 0.30.3__py3-none-any.whl → 0.31.4__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 +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -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 +652 -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 +401 -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 +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -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 +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -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 +370 -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 +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -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/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 +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -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 +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -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.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.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.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing as t
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PluginType(Enum):
|
|
8
|
+
HOOK = "hook"
|
|
9
|
+
WORKFLOW = "workflow"
|
|
10
|
+
INTEGRATION = "integration"
|
|
11
|
+
FORMATTER = "formatter"
|
|
12
|
+
ANALYZER = "analyzer"
|
|
13
|
+
PUBLISHER = "publisher"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PluginMetadata:
|
|
18
|
+
name: str
|
|
19
|
+
version: str
|
|
20
|
+
plugin_type: PluginType
|
|
21
|
+
description: str
|
|
22
|
+
author: str = ""
|
|
23
|
+
license: str = ""
|
|
24
|
+
requires_python: str = ">=3.11"
|
|
25
|
+
dependencies: list[str] = field(default_factory=list)
|
|
26
|
+
entry_point: str = ""
|
|
27
|
+
config_schema: dict[str, t.Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, t.Any]:
|
|
30
|
+
return {
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"version": self.version,
|
|
33
|
+
"plugin_type": self.plugin_type.value,
|
|
34
|
+
"description": self.description,
|
|
35
|
+
"author": self.author,
|
|
36
|
+
"license": self.license,
|
|
37
|
+
"requires_python": self.requires_python,
|
|
38
|
+
"dependencies": self.dependencies,
|
|
39
|
+
"entry_point": self.entry_point,
|
|
40
|
+
"config_schema": self.config_schema,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PluginBase(abc.ABC):
|
|
45
|
+
def __init__(self, metadata: PluginMetadata) -> None:
|
|
46
|
+
self.metadata = metadata
|
|
47
|
+
self._enabled = True
|
|
48
|
+
self._config: dict[str, t.Any] = {}
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def name(self) -> str:
|
|
52
|
+
return self.metadata.name
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def version(self) -> str:
|
|
56
|
+
return self.metadata.version
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def plugin_type(self) -> PluginType:
|
|
60
|
+
return self.metadata.plugin_type
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def enabled(self) -> bool:
|
|
64
|
+
return self._enabled
|
|
65
|
+
|
|
66
|
+
def enable(self) -> None:
|
|
67
|
+
self._enabled = True
|
|
68
|
+
|
|
69
|
+
def disable(self) -> None:
|
|
70
|
+
self._enabled = False
|
|
71
|
+
|
|
72
|
+
def configure(self, config: dict[str, t.Any]) -> None:
|
|
73
|
+
self._config = config.copy()
|
|
74
|
+
self.validate_config(config)
|
|
75
|
+
|
|
76
|
+
@abc.abstractmethod
|
|
77
|
+
def activate(self) -> bool:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abc.abstractmethod
|
|
81
|
+
def deactivate(self) -> bool:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def validate_config(self, config: dict[str, t.Any]) -> None:
|
|
85
|
+
schema = self.metadata.config_schema
|
|
86
|
+
|
|
87
|
+
if not schema:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
required_keys = schema.get("required", [])
|
|
91
|
+
for key in required_keys:
|
|
92
|
+
if key not in config:
|
|
93
|
+
msg = f"Required config key '{key}' missing for plugin {self.name}"
|
|
94
|
+
raise ValueError(
|
|
95
|
+
msg,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_config(self, key: str, default: t.Any = None) -> t.Any:
|
|
99
|
+
return self._config.get(key, default)
|
|
100
|
+
|
|
101
|
+
def get_info(self) -> dict[str, t.Any]:
|
|
102
|
+
return {
|
|
103
|
+
"metadata": self.metadata.to_dict(),
|
|
104
|
+
"enabled": self.enabled,
|
|
105
|
+
"config": self._config,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PluginRegistry:
|
|
110
|
+
def __init__(self) -> None:
|
|
111
|
+
self._plugins: dict[str, PluginBase] = {}
|
|
112
|
+
self._plugins_by_type: dict[PluginType, list[PluginBase]] = {}
|
|
113
|
+
|
|
114
|
+
def register(self, plugin: PluginBase) -> bool:
|
|
115
|
+
if plugin.name in self._plugins:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
self._plugins[plugin.name] = plugin
|
|
119
|
+
|
|
120
|
+
plugin_type = plugin.plugin_type
|
|
121
|
+
if plugin_type not in self._plugins_by_type:
|
|
122
|
+
self._plugins_by_type[plugin_type] = []
|
|
123
|
+
self._plugins_by_type[plugin_type].append(plugin)
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def unregister(self, plugin_name: str) -> bool:
|
|
128
|
+
if plugin_name not in self._plugins:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
plugin = self._plugins.pop(plugin_name)
|
|
132
|
+
|
|
133
|
+
plugin_type = plugin.plugin_type
|
|
134
|
+
if plugin_type in self._plugins_by_type:
|
|
135
|
+
self._plugins_by_type[plugin_type] = [
|
|
136
|
+
p for p in self._plugins_by_type[plugin_type] if p.name != plugin_name
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def get(self, plugin_name: str) -> PluginBase | None:
|
|
142
|
+
return self._plugins.get(plugin_name)
|
|
143
|
+
|
|
144
|
+
def get_by_type(self, plugin_type: PluginType) -> list[PluginBase]:
|
|
145
|
+
return self._plugins_by_type.get(plugin_type, []).copy()
|
|
146
|
+
|
|
147
|
+
def get_enabled(self, plugin_type: PluginType | None = None) -> list[PluginBase]:
|
|
148
|
+
if plugin_type:
|
|
149
|
+
plugins = self.get_by_type(plugin_type)
|
|
150
|
+
else:
|
|
151
|
+
plugins = list(self._plugins.values())
|
|
152
|
+
|
|
153
|
+
return [p for p in plugins if p.enabled]
|
|
154
|
+
|
|
155
|
+
def list_all(self) -> dict[str, PluginBase]:
|
|
156
|
+
return self._plugins.copy()
|
|
157
|
+
|
|
158
|
+
def activate_all(self) -> dict[str, bool]:
|
|
159
|
+
results = {}
|
|
160
|
+
for plugin in self._plugins.values():
|
|
161
|
+
if plugin.enabled:
|
|
162
|
+
try:
|
|
163
|
+
results[plugin.name] = plugin.activate()
|
|
164
|
+
except Exception:
|
|
165
|
+
results[plugin.name] = False
|
|
166
|
+
|
|
167
|
+
return results
|
|
168
|
+
|
|
169
|
+
def deactivate_all(self) -> dict[str, bool]:
|
|
170
|
+
results = {}
|
|
171
|
+
for plugin in self._plugins.values():
|
|
172
|
+
try:
|
|
173
|
+
results[plugin.name] = plugin.deactivate()
|
|
174
|
+
except Exception:
|
|
175
|
+
results[plugin.name] = False
|
|
176
|
+
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
def get_stats(self) -> dict[str, t.Any]:
|
|
180
|
+
by_type = {}
|
|
181
|
+
for plugin_type in PluginType:
|
|
182
|
+
plugins = self.get_by_type(plugin_type)
|
|
183
|
+
by_type[plugin_type.value] = {
|
|
184
|
+
"total": len(plugins),
|
|
185
|
+
"enabled": len([p for p in plugins if p.enabled]),
|
|
186
|
+
"disabled": len([p for p in plugins if not p.enabled]),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"total_plugins": len(self._plugins),
|
|
191
|
+
"enabled_plugins": len([p for p in self._plugins.values() if p.enabled]),
|
|
192
|
+
"by_type": by_type,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_registry = PluginRegistry()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_plugin_registry() -> PluginRegistry:
|
|
200
|
+
return _registry
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import subprocess
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from crackerjack.config.hooks import HookDefinition, HookStage
|
|
9
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
10
|
+
from crackerjack.models.task import HookResult
|
|
11
|
+
|
|
12
|
+
from .base import PluginBase, PluginMetadata, PluginType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CustomHookDefinition:
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
command: list[str] | None = None
|
|
20
|
+
file_patterns: list[str] = field(default_factory=list)
|
|
21
|
+
timeout: int = 60
|
|
22
|
+
stage: HookStage = HookStage.COMPREHENSIVE
|
|
23
|
+
requires_files: bool = True
|
|
24
|
+
parallel_safe: bool = True
|
|
25
|
+
|
|
26
|
+
def to_hook_definition(self) -> HookDefinition:
|
|
27
|
+
cmd = self.command or []
|
|
28
|
+
return HookDefinition(
|
|
29
|
+
name=self.name,
|
|
30
|
+
command=cmd,
|
|
31
|
+
timeout=self.timeout,
|
|
32
|
+
stage=self.stage,
|
|
33
|
+
manual_stage=self.stage == HookStage.COMPREHENSIVE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HookPluginBase(PluginBase, abc.ABC):
|
|
38
|
+
def __init__(self, metadata: PluginMetadata) -> None:
|
|
39
|
+
super().__init__(metadata)
|
|
40
|
+
assert metadata.plugin_type == PluginType.HOOK
|
|
41
|
+
self.console: Console | None = None
|
|
42
|
+
self.pkg_path: Path | None = None
|
|
43
|
+
|
|
44
|
+
def initialize(self, console: Console, pkg_path: Path) -> None:
|
|
45
|
+
self.console = console
|
|
46
|
+
self.pkg_path = pkg_path
|
|
47
|
+
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
def get_hook_definitions(self) -> list[CustomHookDefinition]:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def execute_hook(
|
|
54
|
+
self,
|
|
55
|
+
hook_name: str,
|
|
56
|
+
files: list[Path],
|
|
57
|
+
options: OptionsProtocol,
|
|
58
|
+
) -> HookResult:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def should_run_hook(self, hook_name: str, files: list[Path]) -> bool:
|
|
62
|
+
hook_def = self._get_hook_definition(hook_name)
|
|
63
|
+
if not hook_def or not hook_def.requires_files:
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
if not hook_def.file_patterns:
|
|
67
|
+
return bool(files)
|
|
68
|
+
|
|
69
|
+
for file_path in files:
|
|
70
|
+
for pattern in hook_def.file_patterns:
|
|
71
|
+
if file_path.match(pattern):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def _get_hook_definition(self, hook_name: str) -> CustomHookDefinition | None:
|
|
77
|
+
for hook_def in self.get_hook_definitions():
|
|
78
|
+
if hook_def.name == hook_name:
|
|
79
|
+
return hook_def
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CustomHookPlugin(HookPluginBase):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
metadata: PluginMetadata,
|
|
87
|
+
hook_definitions: list[CustomHookDefinition],
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__(metadata)
|
|
90
|
+
self._hook_definitions = hook_definitions
|
|
91
|
+
|
|
92
|
+
def get_hook_definitions(self) -> list[CustomHookDefinition]:
|
|
93
|
+
return self._hook_definitions.copy()
|
|
94
|
+
|
|
95
|
+
def execute_hook(
|
|
96
|
+
self,
|
|
97
|
+
hook_name: str,
|
|
98
|
+
files: list[Path],
|
|
99
|
+
options: OptionsProtocol,
|
|
100
|
+
) -> HookResult:
|
|
101
|
+
hook_def = self._get_hook_definition(hook_name)
|
|
102
|
+
if not hook_def:
|
|
103
|
+
return HookResult(
|
|
104
|
+
id=hook_name,
|
|
105
|
+
name=hook_name,
|
|
106
|
+
status="error",
|
|
107
|
+
duration=0.0,
|
|
108
|
+
issues_found=[f"Hook definition not found: {hook_name}"],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not hook_def.command:
|
|
112
|
+
return HookResult(
|
|
113
|
+
id=hook_name,
|
|
114
|
+
name=hook_name,
|
|
115
|
+
status="error",
|
|
116
|
+
duration=0.0,
|
|
117
|
+
issues_found=[f"No command defined for hook: {hook_name}"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return self._execute_command_hook(hook_def, files)
|
|
121
|
+
|
|
122
|
+
def _execute_command_hook(
|
|
123
|
+
self,
|
|
124
|
+
hook_def: CustomHookDefinition,
|
|
125
|
+
files: list[Path],
|
|
126
|
+
) -> HookResult:
|
|
127
|
+
import time
|
|
128
|
+
|
|
129
|
+
start_time = time.time()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
cmd = hook_def.command.copy()
|
|
133
|
+
if hook_def.requires_files and files:
|
|
134
|
+
cmd.extend(str(f) for f in files)
|
|
135
|
+
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
cmd,
|
|
138
|
+
check=False,
|
|
139
|
+
cwd=self.pkg_path,
|
|
140
|
+
capture_output=True,
|
|
141
|
+
text=True,
|
|
142
|
+
timeout=hook_def.timeout,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
duration = time.time() - start_time
|
|
146
|
+
status = "passed" if result.returncode == 0 else "failed"
|
|
147
|
+
|
|
148
|
+
issues = [result.stderr] if result.returncode != 0 and result.stderr else []
|
|
149
|
+
return HookResult(
|
|
150
|
+
id=hook_def.name,
|
|
151
|
+
name=hook_def.name,
|
|
152
|
+
status=status,
|
|
153
|
+
duration=duration,
|
|
154
|
+
issues_found=issues,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
except subprocess.TimeoutExpired:
|
|
158
|
+
return HookResult(
|
|
159
|
+
id=hook_def.name,
|
|
160
|
+
name=hook_def.name,
|
|
161
|
+
status="timeout",
|
|
162
|
+
duration=time.time() - start_time,
|
|
163
|
+
issues_found=[f"Hook timed out after {hook_def.timeout}s"],
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return HookResult(
|
|
167
|
+
id=hook_def.name,
|
|
168
|
+
name=hook_def.name,
|
|
169
|
+
status="error",
|
|
170
|
+
duration=time.time() - start_time,
|
|
171
|
+
issues_found=[f"Execution error: {e}"],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def activate(self) -> bool:
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def deactivate(self) -> bool:
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class HookPluginRegistry:
|
|
182
|
+
def __init__(self) -> None:
|
|
183
|
+
self._hook_plugins: dict[str, HookPluginBase] = {}
|
|
184
|
+
|
|
185
|
+
def register_hook_plugin(self, plugin: HookPluginBase) -> bool:
|
|
186
|
+
if plugin.name in self._hook_plugins:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
self._hook_plugins[plugin.name] = plugin
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def unregister_hook_plugin(self, plugin_name: str) -> bool:
|
|
193
|
+
return self._hook_plugins.pop(plugin_name, None) is not None
|
|
194
|
+
|
|
195
|
+
def get_all_custom_hooks(self) -> dict[str, CustomHookDefinition]:
|
|
196
|
+
hooks = {}
|
|
197
|
+
|
|
198
|
+
for plugin in self._hook_plugins.values():
|
|
199
|
+
if not plugin.enabled:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
for hook_def in plugin.get_hook_definitions():
|
|
203
|
+
if hook_def.name not in hooks:
|
|
204
|
+
hooks[hook_def.name] = hook_def
|
|
205
|
+
|
|
206
|
+
return hooks
|
|
207
|
+
|
|
208
|
+
def execute_custom_hook(
|
|
209
|
+
self,
|
|
210
|
+
hook_name: str,
|
|
211
|
+
files: list[Path],
|
|
212
|
+
options: OptionsProtocol,
|
|
213
|
+
) -> HookResult | None:
|
|
214
|
+
for plugin in self._hook_plugins.values():
|
|
215
|
+
if not plugin.enabled:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
hook_defs = plugin.get_hook_definitions()
|
|
219
|
+
if any(h.name == hook_name for h in hook_defs):
|
|
220
|
+
return plugin.execute_hook(hook_name, files, options)
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def get_hooks_for_files(self, files: list[Path]) -> list[str]:
|
|
225
|
+
applicable_hooks = []
|
|
226
|
+
|
|
227
|
+
for plugin in self._hook_plugins.values():
|
|
228
|
+
if not plugin.enabled:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
for hook_def in plugin.get_hook_definitions():
|
|
232
|
+
if plugin.should_run_hook(hook_def.name, files):
|
|
233
|
+
applicable_hooks.append(hook_def.name)
|
|
234
|
+
|
|
235
|
+
return applicable_hooks
|
|
236
|
+
|
|
237
|
+
def initialize_all_plugins(self, console: Console, pkg_path: Path) -> None:
|
|
238
|
+
for plugin in self._hook_plugins.values():
|
|
239
|
+
plugin.initialize(console, pkg_path)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
_hook_registry = HookPluginRegistry()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_hook_plugin_registry() -> HookPluginRegistry:
|
|
246
|
+
return _hook_registry
|