crackerjack 0.31.10__py3-none-any.whl → 0.31.12__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 +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
crackerjack/dynamic_config.py
CHANGED
|
@@ -5,6 +5,30 @@ from pathlib import Path
|
|
|
5
5
|
import jinja2
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _get_project_root() -> Path:
|
|
9
|
+
"""Get the absolute path to the crackerjack project root."""
|
|
10
|
+
# Start from this file and go up to find the real project root
|
|
11
|
+
current_path = Path(__file__).resolve()
|
|
12
|
+
for parent in current_path.parents:
|
|
13
|
+
if (parent / "pyproject.toml").exists() and (parent / "tools").exists():
|
|
14
|
+
# Verify it's the crackerjack project by checking for unique markers and tools dir
|
|
15
|
+
try:
|
|
16
|
+
pyproject_content = (parent / "pyproject.toml").read_text()
|
|
17
|
+
if (
|
|
18
|
+
'name = "crackerjack"' in pyproject_content
|
|
19
|
+
and (
|
|
20
|
+
parent / "tools" / "validate_regex_patterns_standalone.py"
|
|
21
|
+
).exists()
|
|
22
|
+
):
|
|
23
|
+
return parent
|
|
24
|
+
except Exception:
|
|
25
|
+
# If we can't read the file, continue searching
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
# Fallback to the parent of the crackerjack package directory
|
|
29
|
+
return Path(__file__).resolve().parent.parent
|
|
30
|
+
|
|
31
|
+
|
|
8
32
|
class HookMetadata(t.TypedDict):
|
|
9
33
|
id: str
|
|
10
34
|
name: str | None
|
|
@@ -32,6 +56,23 @@ class ConfigMode(t.TypedDict):
|
|
|
32
56
|
|
|
33
57
|
HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
|
|
34
58
|
"structure": [
|
|
59
|
+
{
|
|
60
|
+
"id": "validate-regex-patterns",
|
|
61
|
+
"name": "validate-regex-patterns",
|
|
62
|
+
"repo": "local",
|
|
63
|
+
"rev": "",
|
|
64
|
+
"tier": 1,
|
|
65
|
+
"time_estimate": 0.3,
|
|
66
|
+
"stages": None,
|
|
67
|
+
"args": None,
|
|
68
|
+
"files": r"\.py$",
|
|
69
|
+
"exclude": r"^\.venv/",
|
|
70
|
+
"additional_dependencies": None,
|
|
71
|
+
"types_or": None,
|
|
72
|
+
"language": "system",
|
|
73
|
+
"entry": f"uv run python {_get_project_root() / 'tools' / 'validate_regex_patterns_standalone.py'}",
|
|
74
|
+
"experimental": False,
|
|
75
|
+
},
|
|
35
76
|
{
|
|
36
77
|
"id": "trailing-whitespace",
|
|
37
78
|
"name": "trailing-whitespace",
|
|
@@ -123,12 +164,12 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
|
|
|
123
164
|
"id": "uv-lock",
|
|
124
165
|
"name": None,
|
|
125
166
|
"repo": "https://github.com/astral-sh/uv-pre-commit",
|
|
126
|
-
"rev": "0.8.
|
|
167
|
+
"rev": "0.8.15",
|
|
127
168
|
"tier": 1,
|
|
128
169
|
"time_estimate": 0.5,
|
|
129
170
|
"stages": None,
|
|
130
171
|
"args": None,
|
|
131
|
-
"files": r"^pyproject\.toml$",
|
|
172
|
+
"files": r"^ pyproject\.toml$",
|
|
132
173
|
"exclude": None,
|
|
133
174
|
"additional_dependencies": None,
|
|
134
175
|
"types_or": None,
|
|
@@ -284,9 +325,9 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
|
|
|
284
325
|
"repo": "https://github.com/rohaquinlop/complexipy-pre-commit",
|
|
285
326
|
"rev": "v3.3.0",
|
|
286
327
|
"tier": 3,
|
|
287
|
-
"time_estimate":
|
|
328
|
+
"time_estimate": 1.0,
|
|
288
329
|
"stages": ["pre-push", "manual"],
|
|
289
|
-
"args": ["-d", "low"],
|
|
330
|
+
"args": ["-d", "low", "--max-complexity-allowed", "15"],
|
|
290
331
|
"files": r"^crackerjack/.*\.py$",
|
|
291
332
|
"exclude": r"^(\.venv/|tests/)",
|
|
292
333
|
"additional_dependencies": None,
|
|
@@ -392,7 +433,7 @@ CONFIG_MODES: dict[str, ConfigMode] = {
|
|
|
392
433
|
PRE_COMMIT_TEMPLATE = """repos:
|
|
393
434
|
{%- for repo in repos %}
|
|
394
435
|
{%- if repo.comment %}
|
|
395
|
-
|
|
436
|
+
|
|
396
437
|
{%- endif %}
|
|
397
438
|
- repo: {{ repo.repo }}
|
|
398
439
|
{%- if repo.rev %}
|
|
@@ -430,7 +471,7 @@ PRE_COMMIT_TEMPLATE = """repos:
|
|
|
430
471
|
{%- endif %}
|
|
431
472
|
{%- endfor %}
|
|
432
473
|
|
|
433
|
-
{%-
|
|
474
|
+
{%-endfor %}
|
|
434
475
|
"""
|
|
435
476
|
|
|
436
477
|
|
crackerjack/errors.py
CHANGED
|
@@ -314,9 +314,9 @@ def _format_error_for_console(error: CrackerjackError, verbose: bool) -> Panel:
|
|
|
314
314
|
content = [error.message]
|
|
315
315
|
|
|
316
316
|
if verbose and error.details:
|
|
317
|
-
content.extend(("\n[white]Details: [/white]", str(error.details)))
|
|
317
|
+
content.extend(("\n[white]Details: [/ white]", str(error.details)))
|
|
318
318
|
if error.recovery:
|
|
319
|
-
content.extend(("\n[green]Recovery suggestion: [/green]", str(error.recovery)))
|
|
319
|
+
content.extend(("\n[green]Recovery suggestion: [/ green]", str(error.recovery)))
|
|
320
320
|
|
|
321
321
|
return Panel(
|
|
322
322
|
"\n".join(content),
|
|
@@ -336,7 +336,7 @@ def handle_error(
|
|
|
336
336
|
) -> None:
|
|
337
337
|
if ai_agent:
|
|
338
338
|
formatted_json = _format_error_for_ai_agent(error, verbose)
|
|
339
|
-
console.print(f"[json]{formatted_json}[/json]")
|
|
339
|
+
console.print(f"[json]{formatted_json}[/ json]")
|
|
340
340
|
else:
|
|
341
341
|
panel = _format_error_for_console(error, verbose)
|
|
342
342
|
console.print(panel)
|
|
@@ -375,5 +375,4 @@ def check_command_result(
|
|
|
375
375
|
|
|
376
376
|
|
|
377
377
|
def format_error_report(error: CrackerjackError) -> str:
|
|
378
|
-
"""Format an error for reporting purposes."""
|
|
379
378
|
return f"Error {error.error_code.value}: {error.message}"
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
|
|
10
|
+
from crackerjack.models.protocols import HookLockManagerProtocol
|
|
10
11
|
from crackerjack.models.task import HookResult
|
|
11
12
|
from crackerjack.services.logging import LoggingContext, get_logger
|
|
12
13
|
|
|
@@ -58,6 +59,7 @@ class AsyncHookExecutor:
|
|
|
58
59
|
max_concurrent: int = 4,
|
|
59
60
|
timeout: int = 300,
|
|
60
61
|
quiet: bool = False,
|
|
62
|
+
hook_lock_manager: HookLockManagerProtocol | None = None,
|
|
61
63
|
) -> None:
|
|
62
64
|
self.console = console
|
|
63
65
|
self.pkg_path = pkg_path
|
|
@@ -68,6 +70,17 @@ class AsyncHookExecutor:
|
|
|
68
70
|
|
|
69
71
|
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
70
72
|
|
|
73
|
+
# Use dependency injection for hook lock manager
|
|
74
|
+
if hook_lock_manager is None:
|
|
75
|
+
# Import here to avoid circular imports
|
|
76
|
+
from crackerjack.executors.hook_lock_manager import (
|
|
77
|
+
hook_lock_manager as default_manager,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.hook_lock_manager = default_manager
|
|
81
|
+
else:
|
|
82
|
+
self.hook_lock_manager = hook_lock_manager
|
|
83
|
+
|
|
71
84
|
async def execute_strategy(
|
|
72
85
|
self,
|
|
73
86
|
strategy: HookStrategy,
|
|
@@ -129,19 +142,34 @@ class AsyncHookExecutor:
|
|
|
129
142
|
performance_gain=performance_gain,
|
|
130
143
|
)
|
|
131
144
|
|
|
145
|
+
def get_lock_statistics(self) -> dict[str, t.Any]:
|
|
146
|
+
"""Get comprehensive lock usage statistics for monitoring."""
|
|
147
|
+
return self.hook_lock_manager.get_lock_stats()
|
|
148
|
+
|
|
149
|
+
def get_comprehensive_status(self) -> dict[str, t.Any]:
|
|
150
|
+
"""Get comprehensive status including lock manager status."""
|
|
151
|
+
return {
|
|
152
|
+
"executor_config": {
|
|
153
|
+
"max_concurrent": self.max_concurrent,
|
|
154
|
+
"timeout": self.timeout,
|
|
155
|
+
"quiet": self.quiet,
|
|
156
|
+
},
|
|
157
|
+
"lock_manager_status": self.hook_lock_manager.get_lock_stats(),
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
def _print_strategy_header(self, strategy: HookStrategy) -> None:
|
|
133
161
|
self.console.print("\n" + "-" * 80)
|
|
134
162
|
if strategy.name == "fast":
|
|
135
163
|
self.console.print(
|
|
136
|
-
"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running code quality checks (async)[/bold bright_white]",
|
|
164
|
+
"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running code quality checks (async)[/ bold bright_white]",
|
|
137
165
|
)
|
|
138
166
|
elif strategy.name == "comprehensive":
|
|
139
167
|
self.console.print(
|
|
140
|
-
"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/bold bright_white]",
|
|
168
|
+
"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/ bold bright_white]",
|
|
141
169
|
)
|
|
142
170
|
else:
|
|
143
171
|
self.console.print(
|
|
144
|
-
f"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/bold bright_white]",
|
|
172
|
+
f"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/ bold bright_white]",
|
|
145
173
|
)
|
|
146
174
|
self.console.print("-" * 80 + "\n")
|
|
147
175
|
|
|
@@ -194,7 +222,30 @@ class AsyncHookExecutor:
|
|
|
194
222
|
|
|
195
223
|
async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
|
|
196
224
|
async with self._semaphore:
|
|
197
|
-
|
|
225
|
+
# Check if hook requires sequential execution
|
|
226
|
+
if self.hook_lock_manager.requires_lock(hook.name):
|
|
227
|
+
self.logger.debug(
|
|
228
|
+
f"Hook {hook.name} requires sequential execution lock"
|
|
229
|
+
)
|
|
230
|
+
if not self.quiet:
|
|
231
|
+
self.console.print(
|
|
232
|
+
f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Acquire hook-specific lock if required (e.g., for complexipy)
|
|
236
|
+
if self.hook_lock_manager.requires_lock(hook.name):
|
|
237
|
+
self.logger.debug(
|
|
238
|
+
f"Hook {hook.name} requires sequential execution lock"
|
|
239
|
+
)
|
|
240
|
+
if not self.quiet:
|
|
241
|
+
self.console.print(
|
|
242
|
+
f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
async with self.hook_lock_manager.acquire_hook_lock(hook.name): # type: ignore
|
|
246
|
+
return await self._run_hook_subprocess(hook)
|
|
247
|
+
else:
|
|
248
|
+
return await self._run_hook_subprocess(hook)
|
|
198
249
|
|
|
199
250
|
async def _run_hook_subprocess(self, hook: HookDefinition) -> HookResult:
|
|
200
251
|
start_time = time.time()
|
|
@@ -210,9 +261,15 @@ class AsyncHookExecutor:
|
|
|
210
261
|
timeout=timeout_val,
|
|
211
262
|
)
|
|
212
263
|
|
|
264
|
+
# Pre-commit must run from repository root, not package directory
|
|
265
|
+
repo_root = (
|
|
266
|
+
self.pkg_path.parent
|
|
267
|
+
if self.pkg_path.name == "crackerjack"
|
|
268
|
+
else self.pkg_path
|
|
269
|
+
)
|
|
213
270
|
process = await asyncio.create_subprocess_exec(
|
|
214
271
|
*cmd,
|
|
215
|
-
cwd=
|
|
272
|
+
cwd=repo_root,
|
|
216
273
|
stdout=asyncio.subprocess.PIPE,
|
|
217
274
|
stderr=asyncio.subprocess.PIPE,
|
|
218
275
|
)
|
|
@@ -419,13 +476,6 @@ class AsyncHookExecutor:
|
|
|
419
476
|
) -> None:
|
|
420
477
|
if success:
|
|
421
478
|
self.console.print(
|
|
422
|
-
f"[green]✅[/green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
|
|
423
|
-
f"(async, {performance_gain: .1f} % faster)",
|
|
424
|
-
)
|
|
425
|
-
else:
|
|
426
|
-
failed_count = sum(1 for r in results if r.status == "failed")
|
|
427
|
-
error_count = sum(1 for r in results if r.status in ("timeout", "error"))
|
|
428
|
-
self.console.print(
|
|
429
|
-
f"[red]❌[/red] {strategy.name.title()} hooks failed: {failed_count} failed, {error_count} errors "
|
|
479
|
+
f"[green]✅[/ green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
|
|
430
480
|
f"(async, {performance_gain: .1f} % faster)",
|
|
431
481
|
)
|
|
@@ -95,7 +95,7 @@ class CachedHookExecutor:
|
|
|
95
95
|
success = all(result.status == "passed" for result in results)
|
|
96
96
|
|
|
97
97
|
self.logger.info(
|
|
98
|
-
f"Cached strategy '{strategy.name}' completed in {total_time: .2f}s
|
|
98
|
+
f"Cached strategy '{strategy.name}' completed in {total_time: .2f}s-"
|
|
99
99
|
f"Success: {success}, Cache hits: {cache_hits}, Cache misses: {cache_misses}",
|
|
100
100
|
)
|
|
101
101
|
|
|
@@ -124,8 +124,8 @@ class CachedHookExecutor:
|
|
|
124
124
|
|
|
125
125
|
def _strategy_affects_python_only(self, strategy: HookStrategy) -> bool:
|
|
126
126
|
python_only_hooks = {
|
|
127
|
-
"ruff
|
|
128
|
-
"ruff
|
|
127
|
+
"ruff-format",
|
|
128
|
+
"ruff-check",
|
|
129
129
|
"pyright",
|
|
130
130
|
"bandit",
|
|
131
131
|
"vulture",
|
|
@@ -140,17 +140,17 @@ class CachedHookExecutor:
|
|
|
140
140
|
|
|
141
141
|
def _should_ignore_file(self, file_path: Path) -> bool:
|
|
142
142
|
ignore_patterns = [
|
|
143
|
-
".git/",
|
|
144
|
-
".venv/",
|
|
145
|
-
"__pycache__/",
|
|
146
|
-
".pytest_cache/",
|
|
143
|
+
".git /",
|
|
144
|
+
".venv /",
|
|
145
|
+
"__pycache__ /",
|
|
146
|
+
".pytest_cache /",
|
|
147
147
|
".coverage",
|
|
148
|
-
".crackerjack_cache/",
|
|
149
|
-
"node_modules/",
|
|
150
|
-
".tox/",
|
|
151
|
-
"dist/",
|
|
152
|
-
"build/",
|
|
153
|
-
".egg
|
|
148
|
+
".crackerjack_cache /",
|
|
149
|
+
"node_modules /",
|
|
150
|
+
".tox /",
|
|
151
|
+
"dist /",
|
|
152
|
+
"build /",
|
|
153
|
+
".egg-info /",
|
|
154
154
|
]
|
|
155
155
|
|
|
156
156
|
path_str = str(file_path)
|
|
@@ -188,7 +188,7 @@ class SmartCacheManager:
|
|
|
188
188
|
hook_name: str,
|
|
189
189
|
project_state: dict[str, t.Any],
|
|
190
190
|
) -> bool:
|
|
191
|
-
external_hooks = {"detect
|
|
191
|
+
external_hooks = {"detect-secrets"}
|
|
192
192
|
if hook_name in external_hooks:
|
|
193
193
|
return False
|
|
194
194
|
|
|
@@ -10,6 +10,7 @@ from rich.console import Console
|
|
|
10
10
|
|
|
11
11
|
from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
|
|
12
12
|
from crackerjack.models.task import HookResult
|
|
13
|
+
from crackerjack.services.security_logger import get_security_logger
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@dataclass
|
|
@@ -169,22 +170,43 @@ class HookExecutor:
|
|
|
169
170
|
def _run_hook_subprocess(
|
|
170
171
|
self, hook: HookDefinition
|
|
171
172
|
) -> subprocess.CompletedProcess[str]:
|
|
172
|
-
"""
|
|
173
|
+
"""Run hook subprocess with comprehensive security validation."""
|
|
174
|
+
# Get sanitized environment
|
|
173
175
|
clean_env = self._get_clean_environment()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
176
|
+
|
|
177
|
+
# Use secure subprocess execution
|
|
178
|
+
try:
|
|
179
|
+
# Pre-commit must run from repository root
|
|
180
|
+
# For crackerjack package structure, the repo root is pkg_path itself
|
|
181
|
+
repo_root = self.pkg_path
|
|
182
|
+
# Pre-commit has compatibility issues with secure subprocess
|
|
183
|
+
# Use direct subprocess execution for hooks
|
|
184
|
+
return subprocess.run(
|
|
185
|
+
hook.get_command(),
|
|
186
|
+
cwd=repo_root,
|
|
187
|
+
env=clean_env,
|
|
188
|
+
timeout=hook.timeout,
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
check=False, # Don't raise on non-zero exit codes
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Log security issues but convert to subprocess-compatible result
|
|
195
|
+
security_logger = get_security_logger()
|
|
196
|
+
security_logger.log_subprocess_failure(
|
|
197
|
+
command=hook.get_command(),
|
|
198
|
+
exit_code=-1,
|
|
199
|
+
error_output=str(e),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Return a failed CompletedProcess for consistency
|
|
203
|
+
return subprocess.CompletedProcess(
|
|
204
|
+
args=hook.get_command(), returncode=1, stdout="", stderr=str(e)
|
|
205
|
+
)
|
|
183
206
|
|
|
184
207
|
def _display_hook_output_if_needed(
|
|
185
208
|
self, result: subprocess.CompletedProcess[str]
|
|
186
209
|
) -> None:
|
|
187
|
-
"""Display hook output in verbose mode for failed hooks."""
|
|
188
210
|
if result.returncode == 0 or not self.verbose:
|
|
189
211
|
return
|
|
190
212
|
|
|
@@ -199,9 +221,18 @@ class HookExecutor:
|
|
|
199
221
|
result: subprocess.CompletedProcess[str],
|
|
200
222
|
duration: float,
|
|
201
223
|
) -> HookResult:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
# Formatting hooks return 1 when they fix files, which is success
|
|
225
|
+
if hook.is_formatting and result.returncode == 1:
|
|
226
|
+
# Check if files were modified (successful formatting)
|
|
227
|
+
output_text = result.stdout + result.stderr
|
|
228
|
+
if "files were modified by this hook" in output_text:
|
|
229
|
+
status = "passed"
|
|
230
|
+
else:
|
|
231
|
+
status = "failed"
|
|
232
|
+
else:
|
|
233
|
+
status = "passed" if result.returncode == 0 else "failed"
|
|
234
|
+
|
|
235
|
+
issues_found = self._extract_issues_from_process_output(hook, result, status)
|
|
205
236
|
|
|
206
237
|
return HookResult(
|
|
207
238
|
id=hook.name,
|
|
@@ -214,37 +245,41 @@ class HookExecutor:
|
|
|
214
245
|
)
|
|
215
246
|
|
|
216
247
|
def _extract_issues_from_process_output(
|
|
217
|
-
self,
|
|
248
|
+
self,
|
|
249
|
+
hook: HookDefinition,
|
|
250
|
+
result: subprocess.CompletedProcess[str],
|
|
251
|
+
status: str,
|
|
218
252
|
) -> list[str]:
|
|
219
|
-
|
|
220
|
-
if result.returncode == 0:
|
|
253
|
+
if status == "passed":
|
|
221
254
|
return []
|
|
222
255
|
|
|
223
256
|
error_output = (result.stdout + result.stderr).strip()
|
|
257
|
+
|
|
258
|
+
# For formatting hooks that successfully modified files, don't report as issues
|
|
259
|
+
if hook.is_formatting and "files were modified by this hook" in error_output:
|
|
260
|
+
return []
|
|
261
|
+
|
|
224
262
|
if error_output:
|
|
225
263
|
return [line.strip() for line in error_output.split("\n") if line.strip()]
|
|
226
264
|
|
|
227
|
-
# Fallback to generic message if no output captured
|
|
228
265
|
return [f"Hook failed with code {result.returncode}"]
|
|
229
266
|
|
|
230
267
|
def _create_timeout_result(
|
|
231
268
|
self, hook: HookDefinition, start_time: float
|
|
232
269
|
) -> HookResult:
|
|
233
|
-
"""Create HookResult for timeout scenarios."""
|
|
234
270
|
duration = time.time() - start_time
|
|
235
271
|
return HookResult(
|
|
236
272
|
id=hook.name,
|
|
237
273
|
name=hook.name,
|
|
238
274
|
status="timeout",
|
|
239
275
|
duration=duration,
|
|
240
|
-
issues_found=[f"Hook timed out after {duration
|
|
276
|
+
issues_found=[f"Hook timed out after {duration: .1f}s"],
|
|
241
277
|
stage=hook.stage.value,
|
|
242
278
|
)
|
|
243
279
|
|
|
244
280
|
def _create_error_result(
|
|
245
281
|
self, hook: HookDefinition, start_time: float, error: Exception
|
|
246
282
|
) -> HookResult:
|
|
247
|
-
"""Create HookResult for general exception scenarios."""
|
|
248
283
|
duration = time.time() - start_time
|
|
249
284
|
return HookResult(
|
|
250
285
|
id=hook.name,
|
|
@@ -271,14 +306,11 @@ class HookExecutor:
|
|
|
271
306
|
def _display_hook_result(self, result: HookResult) -> None:
|
|
272
307
|
status_icon = "✅" if result.status == "passed" else "❌"
|
|
273
308
|
|
|
274
|
-
|
|
275
|
-
max_width = 70 # Leave room for terminal margins
|
|
309
|
+
max_width = 70
|
|
276
310
|
|
|
277
311
|
if len(result.name) > max_width:
|
|
278
|
-
# Truncate long names
|
|
279
312
|
line = result.name[: max_width - 3] + "..."
|
|
280
313
|
else:
|
|
281
|
-
# Fill with dots to reach max width
|
|
282
314
|
dots_needed = max_width - len(result.name)
|
|
283
315
|
line = result.name + ("." * dots_needed)
|
|
284
316
|
|
|
@@ -344,6 +376,13 @@ class HookExecutor:
|
|
|
344
376
|
return updated_results
|
|
345
377
|
|
|
346
378
|
def _get_clean_environment(self) -> dict[str, str]:
|
|
379
|
+
"""
|
|
380
|
+
Get a sanitized environment for hook execution.
|
|
381
|
+
|
|
382
|
+
This method now delegates to the secure subprocess utilities
|
|
383
|
+
for comprehensive environment sanitization with security logging.
|
|
384
|
+
"""
|
|
385
|
+
# Create base environment with essential variables
|
|
347
386
|
clean_env = {
|
|
348
387
|
"HOME": os.environ.get("HOME", ""),
|
|
349
388
|
"USER": os.environ.get("USER", ""),
|
|
@@ -353,12 +392,14 @@ class HookExecutor:
|
|
|
353
392
|
"TERM": os.environ.get("TERM", "xterm-256color"),
|
|
354
393
|
}
|
|
355
394
|
|
|
395
|
+
# Handle PATH sanitization with venv filtering
|
|
356
396
|
system_path = os.environ.get("PATH", "")
|
|
357
397
|
if system_path:
|
|
358
398
|
venv_bin = str(Path(self.pkg_path) / ".venv" / "bin")
|
|
359
399
|
path_parts = [p for p in system_path.split(":") if p != venv_bin]
|
|
360
400
|
clean_env["PATH"] = ":".join(path_parts)
|
|
361
401
|
|
|
402
|
+
# Define Python-specific variables to exclude
|
|
362
403
|
python_vars_to_exclude = {
|
|
363
404
|
"VIRTUAL_ENV",
|
|
364
405
|
"PYTHONPATH",
|
|
@@ -366,12 +407,41 @@ class HookExecutor:
|
|
|
366
407
|
"PIP_CONFIG_FILE",
|
|
367
408
|
"PYTHONHOME",
|
|
368
409
|
"CONDA_DEFAULT_ENV",
|
|
410
|
+
"PIPENV_ACTIVE",
|
|
411
|
+
"POETRY_ACTIVE",
|
|
369
412
|
}
|
|
370
413
|
|
|
414
|
+
# Add other safe environment variables
|
|
415
|
+
security_logger = get_security_logger()
|
|
416
|
+
original_count = len(os.environ)
|
|
417
|
+
filtered_count = 0
|
|
418
|
+
|
|
371
419
|
for key, value in os.environ.items():
|
|
372
420
|
if key not in python_vars_to_exclude and key not in clean_env:
|
|
373
|
-
|
|
374
|
-
|
|
421
|
+
# Additional security filtering
|
|
422
|
+
if not key.startswith(
|
|
423
|
+
("PYTHON", "PIP_", "CONDA_", "VIRTUAL_", "__PYVENV")
|
|
424
|
+
):
|
|
425
|
+
# Check for dangerous environment variables
|
|
426
|
+
if key not in {"LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "IFS", "PS4"}:
|
|
427
|
+
clean_env[key] = value
|
|
428
|
+
else:
|
|
429
|
+
filtered_count += 1
|
|
430
|
+
security_logger.log_environment_variable_filtered(
|
|
431
|
+
variable_name=key,
|
|
432
|
+
reason="dangerous environment variable",
|
|
433
|
+
value_preview=(value[:50] if value else "")[:50],
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
filtered_count += 1
|
|
437
|
+
|
|
438
|
+
# Log environment sanitization if significant filtering occurred
|
|
439
|
+
if filtered_count > 5: # Only log if substantial filtering
|
|
440
|
+
security_logger.log_subprocess_environment_sanitized(
|
|
441
|
+
original_count=original_count,
|
|
442
|
+
sanitized_count=len(clean_env),
|
|
443
|
+
filtered_vars=[], # Don't expose all filtered vars for performance
|
|
444
|
+
)
|
|
375
445
|
|
|
376
446
|
return clean_env
|
|
377
447
|
|
|
@@ -381,13 +451,6 @@ class HookExecutor:
|
|
|
381
451
|
results: list[HookResult],
|
|
382
452
|
success: bool,
|
|
383
453
|
) -> None:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
else:
|
|
389
|
-
failed_count = sum(1 for r in results if r.status == "failed")
|
|
390
|
-
error_count = sum(1 for r in results if r.status in ("timeout", "error"))
|
|
391
|
-
self.console.print(
|
|
392
|
-
f"[red]❌[/red] {strategy.name.title()} hooks failed: {failed_count} failed, {error_count} errors",
|
|
393
|
-
)
|
|
454
|
+
# Summary is handled by PhaseCoordinator to avoid duplicate messages
|
|
455
|
+
# Individual hook results are already displayed above
|
|
456
|
+
pass
|