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.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -299
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +618 -928
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +94 -103
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/crackerjack.py +0 -3805
  151. crackerjack/pyproject.toml +0 -286
  152. crackerjack-0.30.3.dist-info/METADATA +0 -1290
  153. crackerjack-0.30.3.dist-info/RECORD +0 -16
  154. {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  155. {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.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, capture_output=True, text=True, cwd=self.working_dir, **kwargs
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('"""') or 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
+ ]