crackerjack 0.31.10__py3-none-any.whl → 0.31.13__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 +50 -9
- 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.13.dist-info}/METADATA +197 -12
- crackerjack-0.31.13.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.13.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
crackerjack/services/git.py
CHANGED
|
@@ -3,35 +3,63 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
|
+
from .secure_subprocess import execute_secure_subprocess
|
|
7
|
+
from .security_logger import get_security_logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FailedGitResult:
|
|
11
|
+
"""A Git result object compatible with subprocess.CompletedProcess."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, command: list[str], error: str) -> None:
|
|
14
|
+
self.args = command
|
|
15
|
+
self.returncode = -1
|
|
16
|
+
self.stdout = ""
|
|
17
|
+
self.stderr = f"Git security validation failed: {error}"
|
|
18
|
+
|
|
6
19
|
|
|
7
20
|
class GitService:
|
|
8
21
|
def __init__(self, console: Console, pkg_path: Path | None = None) -> None:
|
|
9
22
|
self.console = console
|
|
10
23
|
self.pkg_path = pkg_path or Path.cwd()
|
|
11
24
|
|
|
12
|
-
def _run_git_command(
|
|
25
|
+
def _run_git_command(
|
|
26
|
+
self, args: list[str]
|
|
27
|
+
) -> subprocess.CompletedProcess[str] | FailedGitResult:
|
|
28
|
+
"""Execute Git commands with secure subprocess validation."""
|
|
13
29
|
cmd = ["git", *args]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
return execute_secure_subprocess(
|
|
33
|
+
command=cmd,
|
|
34
|
+
cwd=self.pkg_path,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
timeout=60,
|
|
38
|
+
check=False, # Don't raise on non-zero exit codes
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
# Log security issues but return a compatible result
|
|
42
|
+
security_logger = get_security_logger()
|
|
43
|
+
security_logger.log_subprocess_failure(
|
|
44
|
+
command=cmd,
|
|
45
|
+
exit_code=-1,
|
|
46
|
+
error_output=str(e),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Create compatible result for Git operations
|
|
50
|
+
return FailedGitResult(cmd, str(e))
|
|
22
51
|
|
|
23
52
|
def is_git_repo(self) -> bool:
|
|
24
53
|
try:
|
|
25
|
-
result = self._run_git_command(["rev-parse", "
|
|
54
|
+
result = self._run_git_command(["rev-parse", "- - git-dir"])
|
|
26
55
|
return result.returncode == 0
|
|
27
56
|
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
28
57
|
return False
|
|
29
58
|
|
|
30
59
|
def get_changed_files(self) -> list[str]:
|
|
31
60
|
try:
|
|
32
|
-
# Get staged files excluding deletions
|
|
33
61
|
staged_result = self._run_git_command(
|
|
34
|
-
["diff", "--cached", "
|
|
62
|
+
["diff", "--cached", "- - name-only", "- - diff-filter=ACMRT"]
|
|
35
63
|
)
|
|
36
64
|
staged_files = (
|
|
37
65
|
staged_result.stdout.strip().split("\n")
|
|
@@ -39,9 +67,8 @@ class GitService:
|
|
|
39
67
|
else []
|
|
40
68
|
)
|
|
41
69
|
|
|
42
|
-
# Get unstaged files excluding deletions
|
|
43
70
|
unstaged_result = self._run_git_command(
|
|
44
|
-
["diff", "
|
|
71
|
+
["diff", "- - name-only", "- - diff-filter=ACMRT"]
|
|
45
72
|
)
|
|
46
73
|
unstaged_files = (
|
|
47
74
|
unstaged_result.stdout.strip().split("\n")
|
|
@@ -49,9 +76,8 @@ class GitService:
|
|
|
49
76
|
else []
|
|
50
77
|
)
|
|
51
78
|
|
|
52
|
-
# Get untracked files
|
|
53
79
|
untracked_result = self._run_git_command(
|
|
54
|
-
["ls-files", "--others", "
|
|
80
|
+
["ls-files", "--others", "- - exclude-standard"],
|
|
55
81
|
)
|
|
56
82
|
untracked_files = (
|
|
57
83
|
untracked_result.stdout.strip().split("\n")
|
|
@@ -62,15 +88,15 @@ class GitService:
|
|
|
62
88
|
all_files = set(staged_files + unstaged_files + untracked_files)
|
|
63
89
|
return [f for f in all_files if f]
|
|
64
90
|
except Exception as e:
|
|
65
|
-
self.console.print(f"[yellow]⚠️[/yellow] Error getting changed files: {e}")
|
|
91
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Error getting changed files: {e}")
|
|
66
92
|
return []
|
|
67
93
|
|
|
68
94
|
def get_staged_files(self) -> list[str]:
|
|
69
95
|
try:
|
|
70
|
-
result = self._run_git_command(["diff", "--cached", "
|
|
96
|
+
result = self._run_git_command(["diff", "--cached", "- - name-only"])
|
|
71
97
|
return result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
72
98
|
except Exception as e:
|
|
73
|
-
self.console.print(f"[yellow]⚠️[/yellow] Error getting staged files: {e}")
|
|
99
|
+
self.console.print(f"[yellow]⚠️[/ yellow] Error getting staged files: {e}")
|
|
74
100
|
return []
|
|
75
101
|
|
|
76
102
|
def add_files(self, files: list[str]) -> bool:
|
|
@@ -79,30 +105,29 @@ class GitService:
|
|
|
79
105
|
result = self._run_git_command(["add", file])
|
|
80
106
|
if result.returncode != 0:
|
|
81
107
|
self.console.print(
|
|
82
|
-
f"[red]❌[/red] Failed to add {file}: {result.stderr}",
|
|
108
|
+
f"[red]❌[/ red] Failed to add {file}: {result.stderr}",
|
|
83
109
|
)
|
|
84
110
|
return False
|
|
85
111
|
return True
|
|
86
112
|
except Exception as e:
|
|
87
|
-
self.console.print(f"[red]❌[/red] Error adding files: {e}")
|
|
113
|
+
self.console.print(f"[red]❌[/ red] Error adding files: {e}")
|
|
88
114
|
return False
|
|
89
115
|
|
|
90
116
|
def commit(self, message: str) -> bool:
|
|
91
117
|
try:
|
|
92
|
-
result = self._run_git_command(["commit", "-m", message])
|
|
118
|
+
result = self._run_git_command(["commit", "- m", message])
|
|
93
119
|
if result.returncode == 0:
|
|
94
|
-
self.console.print(f"[green]✅[/green] Committed: {message}")
|
|
120
|
+
self.console.print(f"[green]✅[/ green] Committed: {message}")
|
|
95
121
|
return True
|
|
96
122
|
|
|
97
123
|
return self._handle_commit_failure(result, message)
|
|
98
124
|
except Exception as e:
|
|
99
|
-
self.console.print(f"[red]❌[/red] Error committing: {e}")
|
|
125
|
+
self.console.print(f"[red]❌[/ red] Error committing: {e}")
|
|
100
126
|
return False
|
|
101
127
|
|
|
102
128
|
def _handle_commit_failure(
|
|
103
|
-
self, result: subprocess.CompletedProcess[str], message: str
|
|
129
|
+
self, result: subprocess.CompletedProcess[str] | FailedGitResult, message: str
|
|
104
130
|
) -> bool:
|
|
105
|
-
# Check if pre-commit hooks modified files and need re-staging
|
|
106
131
|
if "files were modified by this hook" in result.stderr:
|
|
107
132
|
return self._retry_commit_after_restage(message)
|
|
108
133
|
|
|
@@ -110,57 +135,56 @@ class GitService:
|
|
|
110
135
|
|
|
111
136
|
def _retry_commit_after_restage(self, message: str) -> bool:
|
|
112
137
|
self.console.print(
|
|
113
|
-
"[yellow]🔄[/yellow] Pre-commit hooks modified files - attempting to re-stage and retry commit"
|
|
138
|
+
"[yellow]🔄[/ yellow] Pre - commit hooks modified files - attempting to re-stage and retry commit"
|
|
114
139
|
)
|
|
115
140
|
|
|
116
|
-
|
|
117
|
-
add_result = self._run_git_command(["add", "-u"])
|
|
141
|
+
add_result = self._run_git_command(["add", "- u"])
|
|
118
142
|
if add_result.returncode != 0:
|
|
119
143
|
self.console.print(
|
|
120
|
-
f"[red]❌[/red] Failed to re-stage files: {add_result.stderr}"
|
|
144
|
+
f"[red]❌[/ red] Failed to re-stage files: {add_result.stderr}"
|
|
121
145
|
)
|
|
122
146
|
return False
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
retry_result = self._run_git_command(["commit", "-m", message])
|
|
148
|
+
retry_result = self._run_git_command(["commit", "- m", message])
|
|
126
149
|
if retry_result.returncode == 0:
|
|
127
150
|
self.console.print(
|
|
128
|
-
f"[green]✅[/green] Committed after re-staging: {message}"
|
|
151
|
+
f"[green]✅[/ green] Committed after re-staging: {message}"
|
|
129
152
|
)
|
|
130
153
|
return True
|
|
131
154
|
|
|
132
155
|
self.console.print(
|
|
133
|
-
f"[red]❌[/red] Commit failed on retry: {retry_result.stderr}"
|
|
156
|
+
f"[red]❌[/ red] Commit failed on retry: {retry_result.stderr}"
|
|
134
157
|
)
|
|
135
158
|
return False
|
|
136
159
|
|
|
137
|
-
def _handle_hook_error(
|
|
138
|
-
|
|
160
|
+
def _handle_hook_error(
|
|
161
|
+
self, result: subprocess.CompletedProcess[str] | FailedGitResult
|
|
162
|
+
) -> bool:
|
|
139
163
|
if "pre-commit" in result.stderr or "hook" in result.stderr.lower():
|
|
140
|
-
self.console.print("[red]❌[/red] Commit blocked by pre-commit hooks")
|
|
164
|
+
self.console.print("[red]❌[/ red] Commit blocked by pre-commit hooks")
|
|
141
165
|
if result.stderr.strip():
|
|
142
166
|
self.console.print(
|
|
143
|
-
f"[yellow]Hook output:[/yellow]\n{result.stderr.strip()}"
|
|
167
|
+
f"[yellow]Hook output: [/ yellow]\n{result.stderr.strip()}"
|
|
144
168
|
)
|
|
145
169
|
else:
|
|
146
|
-
self.console.print(f"[red]❌[/red] Commit failed: {result.stderr}")
|
|
170
|
+
self.console.print(f"[red]❌[/ red] Commit failed: {result.stderr}")
|
|
147
171
|
return False
|
|
148
172
|
|
|
149
173
|
def push(self) -> bool:
|
|
150
174
|
try:
|
|
151
175
|
result = self._run_git_command(["push"])
|
|
152
176
|
if result.returncode == 0:
|
|
153
|
-
self.console.print("[green]✅[/green] Pushed to remote")
|
|
177
|
+
self.console.print("[green]✅[/ green] Pushed to remote")
|
|
154
178
|
return True
|
|
155
|
-
self.console.print(f"[red]❌[/red] Push failed: {result.stderr}")
|
|
179
|
+
self.console.print(f"[red]❌[/ red] Push failed: {result.stderr}")
|
|
156
180
|
return False
|
|
157
181
|
except Exception as e:
|
|
158
|
-
self.console.print(f"[red]❌[/red] Error pushing: {e}")
|
|
182
|
+
self.console.print(f"[red]❌[/ red] Error pushing: {e}")
|
|
159
183
|
return False
|
|
160
184
|
|
|
161
185
|
def get_current_branch(self) -> str | None:
|
|
162
186
|
try:
|
|
163
|
-
result = self._run_git_command(["branch", "
|
|
187
|
+
result = self._run_git_command(["branch", "- - show-current"])
|
|
164
188
|
return result.stdout.strip() if result.returncode == 0 else None
|
|
165
189
|
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
166
190
|
return None
|
|
@@ -176,10 +200,10 @@ class GitService:
|
|
|
176
200
|
|
|
177
201
|
def _categorize_files(self, files: list[str]) -> set[str]:
|
|
178
202
|
categories = {
|
|
179
|
-
"docs": ["README", "CLAUDE", "docs/", ".md"],
|
|
180
|
-
"tests": ["test_", "tests/", "conftest.py"],
|
|
203
|
+
"docs": ["README", "CLAUDE", "docs /", ".md"],
|
|
204
|
+
"tests": ["test_", "tests /", "conftest.py"],
|
|
181
205
|
"config": ["pyproject.toml", ".yaml", ".yml", ".json", ".gitignore"],
|
|
182
|
-
"ci": [".github/", "ci/", ".pre-commit"],
|
|
206
|
+
"ci": [".github /", "ci /", ".pre-commit"],
|
|
183
207
|
"deps": ["requirements", "uv.lock", "Pipfile"],
|
|
184
208
|
}
|
|
185
209
|
file_categories: set[str] = set()
|
|
@@ -205,7 +229,7 @@ class GitService:
|
|
|
205
229
|
"docs": "Update documentation",
|
|
206
230
|
"tests": "Update tests",
|
|
207
231
|
"config": "Update configuration",
|
|
208
|
-
"ci": "Update CI/CD configuration",
|
|
232
|
+
"ci": "Update CI / CD configuration",
|
|
209
233
|
"deps": "Update dependencies",
|
|
210
234
|
}
|
|
211
235
|
return [category_messages.get(category, "Update core functionality")]
|
|
@@ -41,7 +41,7 @@ class ProjectHealth:
|
|
|
41
41
|
return False
|
|
42
42
|
|
|
43
43
|
recent = values[-min_points:]
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
return all(a <= b for a, b in zip(recent, recent[1:]))
|
|
46
46
|
|
|
47
47
|
def _is_trending_down(
|
|
@@ -51,7 +51,7 @@ class ProjectHealth:
|
|
|
51
51
|
return False
|
|
52
52
|
|
|
53
53
|
recent = values[-min_points:]
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
return all(a >= b for a, b in zip(recent, recent[1:]))
|
|
56
56
|
|
|
57
57
|
def get_health_score(self) -> float:
|
|
@@ -88,11 +88,11 @@ class ProjectHealth:
|
|
|
88
88
|
|
|
89
89
|
if self._is_trending_up(self.lint_error_trend):
|
|
90
90
|
recommendations.append(
|
|
91
|
-
"🔧 Lint errors are increasing
|
|
91
|
+
"🔧 Lint errors are increasing-consider running formatting tools",
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
if self._is_trending_down(self.test_coverage_trend):
|
|
95
|
-
recommendations.append("🧪 Test coverage is declining
|
|
95
|
+
recommendations.append("🧪 Test coverage is declining-add more tests")
|
|
96
96
|
|
|
97
97
|
if any(age > 365 for age in self.dependency_age.values()):
|
|
98
98
|
old_deps: list[str] = [
|
|
@@ -104,7 +104,7 @@ class ProjectHealth:
|
|
|
104
104
|
|
|
105
105
|
if self.config_completeness < 0.5:
|
|
106
106
|
recommendations.append(
|
|
107
|
-
"⚙️ Project configuration is incomplete
|
|
107
|
+
"⚙️ Project configuration is incomplete-run crackerjack init",
|
|
108
108
|
)
|
|
109
109
|
elif self.config_completeness < 0.8:
|
|
110
110
|
recommendations.append("⚙️ Project configuration could be improved")
|
|
@@ -179,13 +179,13 @@ class HealthMetricsService:
|
|
|
179
179
|
json.dump(data, f, indent=2)
|
|
180
180
|
except Exception as e:
|
|
181
181
|
self.console.print(
|
|
182
|
-
f"[yellow]Warning: Failed to save health metrics: {e}[/yellow]",
|
|
182
|
+
f"[yellow]Warning: Failed to save health metrics: {e}[/ yellow]",
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
def _count_lint_errors(self) -> int | None:
|
|
186
186
|
with suppress(Exception):
|
|
187
187
|
result = subprocess.run(
|
|
188
|
-
["uv", "run", "ruff", "check", ".", "
|
|
188
|
+
["uv", "run", "ruff", "check", ".", "- - output-format=json"],
|
|
189
189
|
check=False,
|
|
190
190
|
capture_output=True,
|
|
191
191
|
text=True,
|
|
@@ -238,12 +238,12 @@ class HealthMetricsService:
|
|
|
238
238
|
"uv",
|
|
239
239
|
"run",
|
|
240
240
|
"python",
|
|
241
|
-
"-m",
|
|
241
|
+
"- m",
|
|
242
242
|
"pytest",
|
|
243
|
-
"--cov=.",
|
|
244
|
-
"
|
|
243
|
+
"--cov =.",
|
|
244
|
+
"- - cov-report=json",
|
|
245
245
|
"--tb=no",
|
|
246
|
-
"-q",
|
|
246
|
+
"- q",
|
|
247
247
|
"--maxfail=1",
|
|
248
248
|
],
|
|
249
249
|
check=False,
|
|
@@ -322,7 +322,7 @@ class HealthMetricsService:
|
|
|
322
322
|
if not dep_spec or dep_spec.startswith("-"):
|
|
323
323
|
return None
|
|
324
324
|
|
|
325
|
-
for operator in ("
|
|
325
|
+
for operator in ("> =", "< =", "= =", "~=", "! =", ">", "<"):
|
|
326
326
|
if operator in dep_spec:
|
|
327
327
|
return dep_spec.split(operator)[0].strip()
|
|
328
328
|
|
|
@@ -345,13 +345,22 @@ class HealthMetricsService:
|
|
|
345
345
|
def _fetch_package_data(self, package_name: str) -> dict[str, t.Any] | None:
|
|
346
346
|
try:
|
|
347
347
|
import urllib.request
|
|
348
|
+
from urllib.parse import urlparse
|
|
348
349
|
|
|
349
350
|
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
parsed = urlparse(url)
|
|
353
|
+
if parsed.scheme != "https" or parsed.netloc != "pypi.org":
|
|
354
|
+
msg = f"Invalid URL: only https://pypi.org URLs are allowed, got {url}"
|
|
355
|
+
raise ValueError(msg)
|
|
356
|
+
|
|
357
|
+
if not parsed.path.startswith("/pypi/") or not parsed.path.endswith(
|
|
358
|
+
"/json"
|
|
359
|
+
):
|
|
360
|
+
msg = f"Invalid PyPI API path: {parsed.path}"
|
|
353
361
|
raise ValueError(msg)
|
|
354
362
|
|
|
363
|
+
# B310: Safe urllib.urlopen with scheme validation above
|
|
355
364
|
with urllib.request.urlopen(url, timeout=10) as response: # nosec B310
|
|
356
365
|
return json.load(response)
|
|
357
366
|
except Exception:
|
|
@@ -457,7 +466,7 @@ class HealthMetricsService:
|
|
|
457
466
|
def _assess_precommit_config(self) -> tuple[float, int]:
|
|
458
467
|
precommit_files = [
|
|
459
468
|
self.project_root / ".pre-commit-config.yaml",
|
|
460
|
-
self.project_root / ".pre-commit-config.yml",
|
|
469
|
+
self.project_root / ".pre - commit-config.yml",
|
|
461
470
|
]
|
|
462
471
|
score = 0.1 if any(f.exists() for f in precommit_files) else 0.0
|
|
463
472
|
return score, 1
|
|
@@ -489,7 +498,6 @@ class HealthMetricsService:
|
|
|
489
498
|
return health
|
|
490
499
|
|
|
491
500
|
def report_health_status(self, health: ProjectHealth) -> None:
|
|
492
|
-
"""Generate and display comprehensive project health report."""
|
|
493
501
|
health_score = health.get_health_score()
|
|
494
502
|
|
|
495
503
|
self._print_health_summary(health_score)
|
|
@@ -497,18 +505,16 @@ class HealthMetricsService:
|
|
|
497
505
|
self._print_health_recommendations(health)
|
|
498
506
|
|
|
499
507
|
def _print_health_summary(self, health_score: float) -> None:
|
|
500
|
-
"""Print the overall health score with appropriate styling."""
|
|
501
508
|
status_icon, status_text, status_color = self._get_health_status_display(
|
|
502
509
|
health_score,
|
|
503
510
|
)
|
|
504
511
|
|
|
505
|
-
self.console.print("\n[bold]📊 Project Health Report[/bold]")
|
|
512
|
+
self.console.print("\n[bold]📊 Project Health Report[/ bold]")
|
|
506
513
|
self.console.print(
|
|
507
|
-
f"{status_icon} Overall Health: [{status_color}]{status_text} ({health_score
|
|
514
|
+
f"{status_icon} Overall Health: [{status_color}]{status_text} ({health_score: .1 %})[/{status_color}]",
|
|
508
515
|
)
|
|
509
516
|
|
|
510
517
|
def _get_health_status_display(self, health_score: float) -> tuple[str, str, str]:
|
|
511
|
-
"""Get display elements (icon, text, color) for health score."""
|
|
512
518
|
if health_score >= 0.8:
|
|
513
519
|
return "🟢", "Excellent", "green"
|
|
514
520
|
if health_score >= 0.6:
|
|
@@ -518,32 +524,30 @@ class HealthMetricsService:
|
|
|
518
524
|
return "🔴", "Poor", "red"
|
|
519
525
|
|
|
520
526
|
def _print_health_metrics(self, health: ProjectHealth) -> None:
|
|
521
|
-
"""Print detailed health metrics."""
|
|
522
527
|
if health.lint_error_trend:
|
|
523
528
|
recent_errors = health.lint_error_trend[-1]
|
|
524
529
|
self.console.print(f"🔧 Lint Errors: {recent_errors}")
|
|
525
530
|
|
|
526
531
|
if health.test_coverage_trend:
|
|
527
532
|
recent_coverage = health.test_coverage_trend[-1]
|
|
528
|
-
self.console.print(f"🧪 Test Coverage: {recent_coverage
|
|
533
|
+
self.console.print(f"🧪 Test Coverage: {recent_coverage: .1f}%")
|
|
529
534
|
|
|
530
535
|
if health.dependency_age:
|
|
531
536
|
avg_age = sum(health.dependency_age.values()) / len(health.dependency_age)
|
|
532
|
-
self.console.print(f"📦 Avg Dependency Age: {avg_age
|
|
537
|
+
self.console.print(f"📦 Avg Dependency Age: {avg_age: .0f} days")
|
|
533
538
|
|
|
534
|
-
self.console.print(f"⚙️ Config Completeness: {health.config_completeness
|
|
539
|
+
self.console.print(f"⚙️ Config Completeness: {health.config_completeness: .1 %}")
|
|
535
540
|
|
|
536
541
|
def _print_health_recommendations(self, health: ProjectHealth) -> None:
|
|
537
|
-
"""Print health recommendations and init suggestions."""
|
|
538
542
|
recommendations = health.get_recommendations()
|
|
539
543
|
if recommendations:
|
|
540
|
-
self.console.print("\n[bold]💡 Recommendations:[/bold]")
|
|
544
|
+
self.console.print("\n[bold]💡 Recommendations: [/ bold]")
|
|
541
545
|
for rec in recommendations:
|
|
542
546
|
self.console.print(f" {rec}")
|
|
543
547
|
|
|
544
548
|
if health.needs_init():
|
|
545
549
|
self.console.print(
|
|
546
|
-
"\n[bold yellow]⚠️ Consider running `crackerjack --init` to improve project health[/bold yellow]",
|
|
550
|
+
"\n[bold yellow]⚠️ Consider running `crackerjack --init` to improve project health[/ bold yellow]",
|
|
547
551
|
)
|
|
548
552
|
|
|
549
553
|
def get_health_trend_summary(self, days: int = 30) -> dict[str, Any]:
|