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,335 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import importlib.util
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import typing as t
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from crackerjack.config.hooks import HookStage
|
|
9
|
+
|
|
10
|
+
from .base import (
|
|
11
|
+
PluginBase,
|
|
12
|
+
PluginMetadata,
|
|
13
|
+
PluginRegistry,
|
|
14
|
+
PluginType,
|
|
15
|
+
get_plugin_registry,
|
|
16
|
+
)
|
|
17
|
+
from .hooks import CustomHookDefinition, CustomHookPlugin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PluginLoadError(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginLoader:
|
|
25
|
+
def __init__(self, registry: PluginRegistry | None = None) -> None:
|
|
26
|
+
self.registry = registry or get_plugin_registry()
|
|
27
|
+
self.logger = logging.getLogger("crackerjack.plugin_loader")
|
|
28
|
+
|
|
29
|
+
def load_plugin_from_file(self, plugin_file: Path) -> PluginBase:
|
|
30
|
+
if not plugin_file.exists():
|
|
31
|
+
msg = f"Plugin file not found: {plugin_file}"
|
|
32
|
+
raise PluginLoadError(msg)
|
|
33
|
+
|
|
34
|
+
if plugin_file.suffix != ".py":
|
|
35
|
+
msg = f"Plugin file must be .py: {plugin_file}"
|
|
36
|
+
raise PluginLoadError(msg)
|
|
37
|
+
|
|
38
|
+
spec = importlib.util.spec_from_file_location(plugin_file.stem, plugin_file)
|
|
39
|
+
if not spec or not spec.loader:
|
|
40
|
+
msg = f"Could not create module spec for: {plugin_file}"
|
|
41
|
+
raise PluginLoadError(msg)
|
|
42
|
+
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
spec.loader.exec_module(module)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
msg = f"Failed to execute plugin module {plugin_file}: {e}"
|
|
49
|
+
raise PluginLoadError(msg)
|
|
50
|
+
|
|
51
|
+
plugin = self._extract_plugin_from_module(module, plugin_file)
|
|
52
|
+
|
|
53
|
+
if not isinstance(plugin, PluginBase):
|
|
54
|
+
msg = f"Plugin {plugin_file} does not provide a PluginBase instance"
|
|
55
|
+
raise PluginLoadError(
|
|
56
|
+
msg,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return plugin
|
|
60
|
+
|
|
61
|
+
def load_plugin_from_config(self, config_file: Path) -> PluginBase:
|
|
62
|
+
if not config_file.exists():
|
|
63
|
+
msg = f"Plugin config file not found: {config_file}"
|
|
64
|
+
raise PluginLoadError(msg)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if config_file.suffix == ".json":
|
|
68
|
+
with config_file.open() as f:
|
|
69
|
+
config = json.load(f)
|
|
70
|
+
elif config_file.suffix in (".yaml", ".yml"):
|
|
71
|
+
import yaml
|
|
72
|
+
|
|
73
|
+
with config_file.open() as f:
|
|
74
|
+
config = yaml.safe_load(f)
|
|
75
|
+
else:
|
|
76
|
+
msg = f"Unsupported config format: {config_file.suffix}"
|
|
77
|
+
raise PluginLoadError(
|
|
78
|
+
msg,
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
msg = f"Failed to parse config file {config_file}: {e}"
|
|
82
|
+
raise PluginLoadError(msg)
|
|
83
|
+
|
|
84
|
+
return self._create_plugin_from_config(config, config_file)
|
|
85
|
+
|
|
86
|
+
def _extract_plugin_from_module(
|
|
87
|
+
self,
|
|
88
|
+
module: t.Any,
|
|
89
|
+
plugin_file: Path,
|
|
90
|
+
) -> PluginBase:
|
|
91
|
+
plugin = self._try_standard_entry_points(module)
|
|
92
|
+
if plugin:
|
|
93
|
+
return plugin
|
|
94
|
+
|
|
95
|
+
plugin = self._try_plugin_subclasses(module, plugin_file)
|
|
96
|
+
if plugin:
|
|
97
|
+
return plugin
|
|
98
|
+
|
|
99
|
+
msg = f"No valid plugin found in {plugin_file}"
|
|
100
|
+
raise PluginLoadError(msg)
|
|
101
|
+
|
|
102
|
+
def _try_standard_entry_points(self, module: t.Any) -> PluginBase | None:
|
|
103
|
+
entry_points = [
|
|
104
|
+
"plugin",
|
|
105
|
+
"create_plugin",
|
|
106
|
+
"PLUGIN",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
for entry_point in entry_points:
|
|
110
|
+
plugin = self._try_single_entry_point(module, entry_point)
|
|
111
|
+
if plugin:
|
|
112
|
+
return plugin
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _try_single_entry_point(
|
|
117
|
+
self,
|
|
118
|
+
module: t.Any,
|
|
119
|
+
entry_point: str,
|
|
120
|
+
) -> PluginBase | None:
|
|
121
|
+
if not hasattr(module, entry_point):
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
obj = getattr(module, entry_point)
|
|
125
|
+
|
|
126
|
+
if isinstance(obj, PluginBase):
|
|
127
|
+
return obj
|
|
128
|
+
if callable(obj):
|
|
129
|
+
return self._try_factory_function(obj, entry_point)
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _try_factory_function(
|
|
134
|
+
self,
|
|
135
|
+
factory: t.Callable,
|
|
136
|
+
name: str,
|
|
137
|
+
) -> PluginBase | None:
|
|
138
|
+
try:
|
|
139
|
+
result = factory()
|
|
140
|
+
if isinstance(result, PluginBase):
|
|
141
|
+
return result
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self.logger.warning(f"Plugin factory {name} failed: {e}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _try_plugin_subclasses(
|
|
147
|
+
self,
|
|
148
|
+
module: t.Any,
|
|
149
|
+
plugin_file: Path,
|
|
150
|
+
) -> PluginBase | None:
|
|
151
|
+
for name, obj in vars(module).items():
|
|
152
|
+
if self._is_valid_plugin_class(obj):
|
|
153
|
+
plugin = self._try_instantiate_plugin_class(obj, name, plugin_file)
|
|
154
|
+
if plugin:
|
|
155
|
+
return plugin
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _is_valid_plugin_class(self, obj: t.Any) -> bool:
|
|
159
|
+
return (
|
|
160
|
+
isinstance(obj, type)
|
|
161
|
+
and issubclass(obj, PluginBase)
|
|
162
|
+
and obj is not PluginBase
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _try_instantiate_plugin_class(
|
|
166
|
+
self,
|
|
167
|
+
plugin_class: type[PluginBase],
|
|
168
|
+
name: str,
|
|
169
|
+
plugin_file: Path,
|
|
170
|
+
) -> PluginBase | None:
|
|
171
|
+
try:
|
|
172
|
+
metadata = PluginMetadata(
|
|
173
|
+
name=plugin_file.stem,
|
|
174
|
+
version="1.0.0",
|
|
175
|
+
plugin_type=PluginType.HOOK,
|
|
176
|
+
description="Custom plugin",
|
|
177
|
+
)
|
|
178
|
+
return plugin_class(metadata)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.logger.warning(f"Failed to instantiate plugin class {name}: {e}")
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def _create_plugin_from_config(
|
|
184
|
+
self,
|
|
185
|
+
config: dict[str, t.Any],
|
|
186
|
+
config_file: Path,
|
|
187
|
+
) -> PluginBase:
|
|
188
|
+
metadata = PluginMetadata(
|
|
189
|
+
name=config.get("name", config_file.stem),
|
|
190
|
+
version=config.get("version", "1.0.0"),
|
|
191
|
+
plugin_type=PluginType(config.get("type", "hook")),
|
|
192
|
+
description=config.get("description", "Custom plugin"),
|
|
193
|
+
author=config.get("author", ""),
|
|
194
|
+
license=config.get("license", ""),
|
|
195
|
+
dependencies=config.get("dependencies", []),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if metadata.plugin_type == PluginType.HOOK:
|
|
199
|
+
return self._create_hook_plugin_from_config(metadata, config)
|
|
200
|
+
msg = f"Unsupported plugin type: {metadata.plugin_type}"
|
|
201
|
+
raise PluginLoadError(msg)
|
|
202
|
+
|
|
203
|
+
def _create_hook_plugin_from_config(
|
|
204
|
+
self,
|
|
205
|
+
metadata: PluginMetadata,
|
|
206
|
+
config: dict[str, t.Any],
|
|
207
|
+
) -> CustomHookPlugin:
|
|
208
|
+
hooks_config = config.get("hooks", [])
|
|
209
|
+
hook_definitions = []
|
|
210
|
+
|
|
211
|
+
for hook_config in hooks_config:
|
|
212
|
+
hook_def = CustomHookDefinition(
|
|
213
|
+
name=hook_config["name"],
|
|
214
|
+
description=hook_config.get("description", ""),
|
|
215
|
+
command=hook_config.get("command", []),
|
|
216
|
+
file_patterns=hook_config.get("file_patterns", []),
|
|
217
|
+
timeout=hook_config.get("timeout", 60),
|
|
218
|
+
stage=HookStage(hook_config.get("stage", "comprehensive")),
|
|
219
|
+
requires_files=hook_config.get("requires_files", True),
|
|
220
|
+
parallel_safe=hook_config.get("parallel_safe", True),
|
|
221
|
+
)
|
|
222
|
+
hook_definitions.append(hook_def)
|
|
223
|
+
|
|
224
|
+
return CustomHookPlugin(metadata, hook_definitions)
|
|
225
|
+
|
|
226
|
+
def load_and_register(self, plugin_source: Path) -> bool:
|
|
227
|
+
try:
|
|
228
|
+
if plugin_source.suffix == ".py":
|
|
229
|
+
plugin = self.load_plugin_from_file(plugin_source)
|
|
230
|
+
elif plugin_source.suffix in (".json", ".yaml", ".yml"):
|
|
231
|
+
plugin = self.load_plugin_from_config(plugin_source)
|
|
232
|
+
else:
|
|
233
|
+
self.logger.error(f"Unsupported plugin file type: {plugin_source}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
success = self.registry.register(plugin)
|
|
237
|
+
if success:
|
|
238
|
+
self.logger.info(f"Successfully loaded plugin: {plugin.name}")
|
|
239
|
+
else:
|
|
240
|
+
self.logger.warning(f"Plugin already registered: {plugin.name}")
|
|
241
|
+
|
|
242
|
+
return success
|
|
243
|
+
|
|
244
|
+
except PluginLoadError as e:
|
|
245
|
+
self.logger.exception(f"Failed to load plugin from {plugin_source}: {e}")
|
|
246
|
+
return False
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.logger.exception(
|
|
249
|
+
f"Unexpected error loading plugin {plugin_source}: {e}"
|
|
250
|
+
)
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class PluginDiscovery:
|
|
255
|
+
def __init__(self, loader: PluginLoader | None = None) -> None:
|
|
256
|
+
self.loader = loader or PluginLoader()
|
|
257
|
+
self.logger = logging.getLogger("crackerjack.plugin_discovery")
|
|
258
|
+
|
|
259
|
+
def discover_in_directory(
|
|
260
|
+
self,
|
|
261
|
+
directory: Path,
|
|
262
|
+
recursive: bool = False,
|
|
263
|
+
) -> list[Path]:
|
|
264
|
+
if not directory.exists() or not directory.is_dir():
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
plugin_files = []
|
|
268
|
+
|
|
269
|
+
patterns = ["*.py", "*.json", "*.yaml", "*.yml"]
|
|
270
|
+
|
|
271
|
+
for pattern in patterns:
|
|
272
|
+
if recursive:
|
|
273
|
+
plugin_files.extend(directory.rglob(pattern))
|
|
274
|
+
else:
|
|
275
|
+
plugin_files.extend(directory.glob(pattern))
|
|
276
|
+
|
|
277
|
+
return [f for f in plugin_files if self._looks_like_plugin_file(f)]
|
|
278
|
+
|
|
279
|
+
def discover_in_project(self, project_path: Path) -> list[Path]:
|
|
280
|
+
plugin_files = []
|
|
281
|
+
|
|
282
|
+
plugin_dirs = [
|
|
283
|
+
project_path / "plugins",
|
|
284
|
+
project_path / ".cache" / "crackerjack" / "plugins",
|
|
285
|
+
project_path / "tools" / "crackerjack",
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
for plugin_dir in plugin_dirs:
|
|
289
|
+
if plugin_dir.exists():
|
|
290
|
+
plugin_files.extend(
|
|
291
|
+
self.discover_in_directory(plugin_dir, recursive=True),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return plugin_files
|
|
295
|
+
|
|
296
|
+
def load_discovered_plugins(self, plugin_files: list[Path]) -> dict[str, bool]:
|
|
297
|
+
results = {}
|
|
298
|
+
|
|
299
|
+
for plugin_file in plugin_files:
|
|
300
|
+
self.logger.info(f"Loading plugin: {plugin_file}")
|
|
301
|
+
success = self.loader.load_and_register(plugin_file)
|
|
302
|
+
results[str(plugin_file)] = success
|
|
303
|
+
|
|
304
|
+
return results
|
|
305
|
+
|
|
306
|
+
def auto_discover_and_load(self, project_path: Path) -> dict[str, bool]:
|
|
307
|
+
plugin_files = self.discover_in_project(project_path)
|
|
308
|
+
|
|
309
|
+
if plugin_files:
|
|
310
|
+
self.logger.info(f"Found {len(plugin_files)} potential plugin files")
|
|
311
|
+
return self.load_discovered_plugins(plugin_files)
|
|
312
|
+
self.logger.info("No plugin files found")
|
|
313
|
+
return {}
|
|
314
|
+
|
|
315
|
+
def _looks_like_plugin_file(self, file_path: Path) -> bool:
|
|
316
|
+
name_lower = file_path.name.lower()
|
|
317
|
+
|
|
318
|
+
if name_lower.startswith(("test_", "__", ".")):
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
if name_lower in ("__init__.py", "setup.py", "conftest.py"):
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
plugin_indicators = [
|
|
325
|
+
"plugin",
|
|
326
|
+
"hook",
|
|
327
|
+
"extension",
|
|
328
|
+
"addon",
|
|
329
|
+
"crackerjack",
|
|
330
|
+
"check",
|
|
331
|
+
"lint",
|
|
332
|
+
"format",
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
return any(indicator in name_lower for indicator in plugin_indicators)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
8
|
+
|
|
9
|
+
from .base import PluginRegistry, PluginType, get_plugin_registry
|
|
10
|
+
from .hooks import HookPluginRegistry, get_hook_plugin_registry
|
|
11
|
+
from .loader import PluginDiscovery, PluginLoader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PluginManager:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
console: Console,
|
|
18
|
+
project_path: Path,
|
|
19
|
+
registry: PluginRegistry | None = None,
|
|
20
|
+
hook_registry: HookPluginRegistry | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.console = console
|
|
23
|
+
self.project_path = project_path
|
|
24
|
+
self.registry = registry or get_plugin_registry()
|
|
25
|
+
self.hook_registry = hook_registry or get_hook_plugin_registry()
|
|
26
|
+
|
|
27
|
+
self.loader = PluginLoader(self.registry)
|
|
28
|
+
self.discovery = PluginDiscovery(self.loader)
|
|
29
|
+
self.logger = logging.getLogger("crackerjack.plugin_manager")
|
|
30
|
+
|
|
31
|
+
self._initialized = False
|
|
32
|
+
|
|
33
|
+
def initialize(self) -> bool:
|
|
34
|
+
if self._initialized:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
self.logger.info("Initializing plugin system")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
results = self.discovery.auto_discover_and_load(self.project_path)
|
|
41
|
+
|
|
42
|
+
loaded_count = sum(1 for success in results.values() if success)
|
|
43
|
+
total_count = len(results)
|
|
44
|
+
|
|
45
|
+
if total_count > 0:
|
|
46
|
+
self.console.print(
|
|
47
|
+
f"[green]✅[/green] Loaded {loaded_count} / {total_count} plugins",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
activation_results = self.registry.activate_all()
|
|
51
|
+
activated_count = sum(
|
|
52
|
+
1 for success in activation_results.values() if success
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if activated_count > 0:
|
|
56
|
+
self.console.print(
|
|
57
|
+
f"[green]✅[/green] Activated {activated_count} plugins",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
hook_plugins = self.registry.get_enabled(PluginType.HOOK)
|
|
61
|
+
for plugin in hook_plugins:
|
|
62
|
+
if hasattr(plugin, "initialize"):
|
|
63
|
+
plugin.initialize(self.console, self.project_path)
|
|
64
|
+
self.hook_registry.register_hook_plugin(plugin)
|
|
65
|
+
|
|
66
|
+
self._initialized = True
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
self.logger.exception(f"Failed to initialize plugin system: {e}")
|
|
71
|
+
self.console.print(
|
|
72
|
+
f"[red]❌[/red] Plugin system initialization failed: {e}",
|
|
73
|
+
)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def shutdown(self) -> None:
|
|
77
|
+
if not self._initialized:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self.logger.info("Shutting down plugin system")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
results = self.registry.deactivate_all()
|
|
84
|
+
deactivated_count = sum(1 for success in results.values() if success)
|
|
85
|
+
|
|
86
|
+
if deactivated_count > 0:
|
|
87
|
+
self.console.print(
|
|
88
|
+
f"[yellow]⏹️[/yellow] Deactivated {deactivated_count} plugins",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self._initialized = False
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self.logger.exception(f"Error during plugin system shutdown: {e}")
|
|
95
|
+
|
|
96
|
+
def list_plugins(self, plugin_type: PluginType | None = None) -> dict[str, t.Any]:
|
|
97
|
+
if plugin_type:
|
|
98
|
+
plugins = self.registry.get_by_type(plugin_type)
|
|
99
|
+
else:
|
|
100
|
+
plugins = list(self.registry.list_all().values())
|
|
101
|
+
|
|
102
|
+
plugin_info = []
|
|
103
|
+
for plugin in plugins:
|
|
104
|
+
info = plugin.get_info()
|
|
105
|
+
info["active"] = plugin.enabled
|
|
106
|
+
plugin_info.append(info)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"plugins": plugin_info,
|
|
110
|
+
"total": len(plugin_info),
|
|
111
|
+
"enabled": len([p for p in plugins if p.enabled]),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def get_plugin_stats(self) -> dict[str, t.Any]:
|
|
115
|
+
stats = self.registry.get_stats()
|
|
116
|
+
|
|
117
|
+
hook_plugins = self.registry.get_enabled(PluginType.HOOK)
|
|
118
|
+
custom_hooks = self.hook_registry.get_all_custom_hooks()
|
|
119
|
+
|
|
120
|
+
stats["hook_plugins"] = {
|
|
121
|
+
"active_plugins": len(hook_plugins),
|
|
122
|
+
"total_custom_hooks": len(custom_hooks),
|
|
123
|
+
"hook_names": list(custom_hooks.keys()),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return stats
|
|
127
|
+
|
|
128
|
+
def enable_plugin(self, plugin_name: str) -> bool:
|
|
129
|
+
plugin = self.registry.get(plugin_name)
|
|
130
|
+
if not plugin:
|
|
131
|
+
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
if plugin.enabled:
|
|
135
|
+
self.console.print(
|
|
136
|
+
f"[yellow]⚠️[/yellow] Plugin already enabled: {plugin_name}",
|
|
137
|
+
)
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
plugin.enable()
|
|
142
|
+
success = plugin.activate()
|
|
143
|
+
|
|
144
|
+
if success:
|
|
145
|
+
self.console.print(f"[green]✅[/green] Enabled plugin: {plugin_name}")
|
|
146
|
+
|
|
147
|
+
if plugin.plugin_type == PluginType.HOOK:
|
|
148
|
+
self.hook_registry.register_hook_plugin(plugin)
|
|
149
|
+
|
|
150
|
+
return True
|
|
151
|
+
plugin.disable()
|
|
152
|
+
self.console.print(
|
|
153
|
+
f"[red]❌[/red] Failed to activate plugin: {plugin_name}",
|
|
154
|
+
)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.console.print(
|
|
159
|
+
f"[red]❌[/red] Error enabling plugin {plugin_name}: {e}",
|
|
160
|
+
)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def disable_plugin(self, plugin_name: str) -> bool:
|
|
164
|
+
plugin = self.registry.get(plugin_name)
|
|
165
|
+
if not plugin:
|
|
166
|
+
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
if not plugin.enabled:
|
|
170
|
+
self.console.print(
|
|
171
|
+
f"[yellow]⚠️[/yellow] Plugin already disabled: {plugin_name}",
|
|
172
|
+
)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
success = plugin.deactivate()
|
|
177
|
+
plugin.disable()
|
|
178
|
+
|
|
179
|
+
if plugin.plugin_type == PluginType.HOOK:
|
|
180
|
+
self.hook_registry.unregister_hook_plugin(plugin_name)
|
|
181
|
+
|
|
182
|
+
if success:
|
|
183
|
+
self.console.print(f"[yellow]⏹️[/yellow] Disabled plugin: {plugin_name}")
|
|
184
|
+
else:
|
|
185
|
+
self.console.print(
|
|
186
|
+
f"[yellow]⚠️[/yellow] Plugin disabled with warnings: {plugin_name}",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.console.print(
|
|
193
|
+
f"[red]❌[/red] Error disabling plugin {plugin_name}: {e}",
|
|
194
|
+
)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def reload_plugin(self, plugin_name: str) -> bool:
|
|
198
|
+
if not self.disable_plugin(plugin_name):
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
return self.enable_plugin(plugin_name)
|
|
202
|
+
|
|
203
|
+
def configure_plugin(self, plugin_name: str, config: dict[str, t.Any]) -> bool:
|
|
204
|
+
plugin = self.registry.get(plugin_name)
|
|
205
|
+
if not plugin:
|
|
206
|
+
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
plugin.configure(config)
|
|
211
|
+
self.console.print(f"[green]✅[/green] Configured plugin: {plugin_name}")
|
|
212
|
+
return True
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.console.print(
|
|
215
|
+
f"[red]❌[/red] Error configuring plugin {plugin_name}: {e}",
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def install_plugin_from_file(self, plugin_file: Path) -> bool:
|
|
220
|
+
try:
|
|
221
|
+
success = self.loader.load_and_register(plugin_file)
|
|
222
|
+
|
|
223
|
+
if success:
|
|
224
|
+
self.console.print(
|
|
225
|
+
f"[green]✅[/green] Installed plugin from: {plugin_file}",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if self._initialized:
|
|
229
|
+
plugins = self.registry.list_all()
|
|
230
|
+
if plugins:
|
|
231
|
+
latest_plugin_name = max(
|
|
232
|
+
plugins.keys(),
|
|
233
|
+
key=lambda k: id(plugins[k]),
|
|
234
|
+
)
|
|
235
|
+
self.enable_plugin(latest_plugin_name)
|
|
236
|
+
else:
|
|
237
|
+
self.console.print(
|
|
238
|
+
f"[red]❌[/red] Failed to install plugin from: {plugin_file}",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return success
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
self.console.print(
|
|
245
|
+
f"[red]❌[/red] Error installing plugin {plugin_file}: {e}",
|
|
246
|
+
)
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def get_available_custom_hooks(self) -> list[str]:
|
|
250
|
+
custom_hooks = self.hook_registry.get_all_custom_hooks()
|
|
251
|
+
return list(custom_hooks.keys())
|
|
252
|
+
|
|
253
|
+
def execute_custom_hook(
|
|
254
|
+
self,
|
|
255
|
+
hook_name: str,
|
|
256
|
+
files: list[Path],
|
|
257
|
+
options: OptionsProtocol,
|
|
258
|
+
) -> t.Any:
|
|
259
|
+
return self.hook_registry.execute_custom_hook(hook_name, files, options)
|
crackerjack/py313.py
CHANGED
|
@@ -132,7 +132,12 @@ class EnhancedCommandRunner:
|
|
|
132
132
|
start_time = time.time()
|
|
133
133
|
try:
|
|
134
134
|
process = subprocess.run(
|
|
135
|
-
cmd,
|
|
135
|
+
cmd,
|
|
136
|
+
check=False,
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
cwd=self.working_dir,
|
|
140
|
+
**kwargs,
|
|
136
141
|
)
|
|
137
142
|
duration_ms = (time.time() - start_time) * 1000
|
|
138
143
|
return CommandResult(
|
|
@@ -172,13 +177,13 @@ def clean_python_code(code: str) -> str:
|
|
|
172
177
|
continue
|
|
173
178
|
case s if "#" in s and (
|
|
174
179
|
not any(
|
|
175
|
-
skip in s for skip in ("# noqa", "# type:", "# pragma", "# skip")
|
|
180
|
+
skip in s for skip in ("# noqa", "# type: ", "# pragma", "# skip")
|
|
176
181
|
)
|
|
177
182
|
):
|
|
178
183
|
code_part = line.split("#", 1)[0].rstrip()
|
|
179
184
|
if code_part:
|
|
180
185
|
cleaned_lines.append(code_part)
|
|
181
|
-
case s if s.startswith('"""'
|
|
186
|
+
case s if s.startswith(('"""', "'''")):
|
|
182
187
|
continue
|
|
183
188
|
case _:
|
|
184
189
|
cleaned_lines.append(line)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .cache import CacheEntry, CacheStats, CrackerjackCache, FileCache, InMemoryCache
|
|
2
|
+
from .config import ConfigurationService
|
|
3
|
+
from .file_hasher import FileHasher, SmartFileWatcher
|
|
4
|
+
from .filesystem import FileSystemService
|
|
5
|
+
from .git import GitService
|
|
6
|
+
from .initialization import InitializationService
|
|
7
|
+
from .security import SecurityService
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CacheEntry",
|
|
11
|
+
"CacheStats",
|
|
12
|
+
"ConfigurationService",
|
|
13
|
+
"CrackerjackCache",
|
|
14
|
+
"FileCache",
|
|
15
|
+
"FileHasher",
|
|
16
|
+
"FileSystemService",
|
|
17
|
+
"GitService",
|
|
18
|
+
"InMemoryCache",
|
|
19
|
+
"InitializationService",
|
|
20
|
+
"SecurityService",
|
|
21
|
+
"SmartFileWatcher",
|
|
22
|
+
]
|