codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"""Environment validation and tool detection for CodeFRAME.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Tool detection using shutil.which() and version checking
|
|
5
|
+
- Ecosystem-specific tool detectors (Python, JavaScript, Rust, Generic)
|
|
6
|
+
- Project type detection from manifest files
|
|
7
|
+
- Environment validation with health scoring
|
|
8
|
+
- Recommendations for missing or incompatible tools
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from codeframe.core.environment import EnvironmentValidator
|
|
12
|
+
|
|
13
|
+
validator = EnvironmentValidator()
|
|
14
|
+
result = validator.validate_environment(Path("."))
|
|
15
|
+
|
|
16
|
+
if not result.is_healthy:
|
|
17
|
+
for rec in result.recommendations:
|
|
18
|
+
print(rec)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Enums and Constants
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ToolStatus(str, Enum):
|
|
39
|
+
"""Status of a detected tool."""
|
|
40
|
+
|
|
41
|
+
AVAILABLE = "available"
|
|
42
|
+
NOT_FOUND = "not_found"
|
|
43
|
+
VERSION_INCOMPATIBLE = "version_incompatible"
|
|
44
|
+
ERROR = "error"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Minimum version requirements for common tools
|
|
48
|
+
MIN_VERSIONS: dict[str, str] = {
|
|
49
|
+
"python": "3.8.0",
|
|
50
|
+
"node": "16.0.0",
|
|
51
|
+
"npm": "8.0.0",
|
|
52
|
+
"pytest": "7.0.0",
|
|
53
|
+
"ruff": "0.1.0",
|
|
54
|
+
"git": "2.0.0",
|
|
55
|
+
"cargo": "1.60.0",
|
|
56
|
+
"rustc": "1.60.0",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Data Classes
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ToolInfo:
|
|
67
|
+
"""Information about a detected tool."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
path: Optional[str]
|
|
71
|
+
version: Optional[str]
|
|
72
|
+
status: ToolStatus
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_available(self) -> bool:
|
|
76
|
+
"""Check if tool is available and compatible."""
|
|
77
|
+
return self.status == ToolStatus.AVAILABLE
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ValidationResult:
|
|
82
|
+
"""Result of environment validation."""
|
|
83
|
+
|
|
84
|
+
project_type: str
|
|
85
|
+
detected_tools: dict[str, ToolInfo]
|
|
86
|
+
missing_tools: list[str]
|
|
87
|
+
optional_missing: list[str]
|
|
88
|
+
health_score: float
|
|
89
|
+
recommendations: list[str]
|
|
90
|
+
warnings: list[str]
|
|
91
|
+
conflicts: list[str]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_healthy(self) -> bool:
|
|
95
|
+
"""Check if environment is healthy (score >= 0.8)."""
|
|
96
|
+
return self.health_score >= 0.8 and len(self.missing_tools) == 0
|
|
97
|
+
|
|
98
|
+
def is_healthy_with_threshold(self, threshold: float) -> bool:
|
|
99
|
+
"""Check health against custom threshold."""
|
|
100
|
+
return self.health_score >= threshold
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# Version Utilities
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_version(version_str: str) -> Optional[tuple[int, ...]]:
|
|
109
|
+
"""Parse a version string into a tuple of integers.
|
|
110
|
+
|
|
111
|
+
Supported formats:
|
|
112
|
+
- Simple semver: "1.2.3", "1.2"
|
|
113
|
+
- With 'v' prefix: "v1.2.3"
|
|
114
|
+
- With tool name prefix: "Python 3.11.4", "pytest 7.4.0", "node v18.12.0"
|
|
115
|
+
- With pre-release suffix: "1.2.3-rc1", "1.2.3-alpha.1"
|
|
116
|
+
- With post-release suffix: "7.4.0.post1"
|
|
117
|
+
- Git versions: "git version 2.39.0"
|
|
118
|
+
|
|
119
|
+
Limitations (not fully supported):
|
|
120
|
+
- Build metadata is ignored: "1.2.3+build.456" -> (1, 2, 3)
|
|
121
|
+
- Pre-release ordering not considered: "1.2.3-alpha" == "1.2.3-beta" == "1.2.3"
|
|
122
|
+
- Only first 3 version components used: "1.2.3.4" -> (1, 2, 3)
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
version_str: Version string to parse
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Tuple of (major, minor, patch) or None if parsing fails
|
|
129
|
+
"""
|
|
130
|
+
if not version_str:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Remove common prefixes
|
|
134
|
+
cleaned = version_str.strip()
|
|
135
|
+
|
|
136
|
+
# Extract version-like pattern from the string
|
|
137
|
+
# Match patterns like: 1.2.3, v1.2.3, 1.2
|
|
138
|
+
pattern = r"(\d+)\.(\d+)(?:\.(\d+))?"
|
|
139
|
+
match = re.search(pattern, cleaned)
|
|
140
|
+
|
|
141
|
+
if not match:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
major = int(match.group(1))
|
|
145
|
+
minor = int(match.group(2))
|
|
146
|
+
patch = int(match.group(3)) if match.group(3) else 0
|
|
147
|
+
|
|
148
|
+
return (major, minor, patch)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def compare_versions(version1: str, version2: str) -> int:
|
|
152
|
+
"""Compare two version strings.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
version1: First version string
|
|
156
|
+
version2: Second version string
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
-1 if version1 < version2
|
|
160
|
+
0 if version1 == version2
|
|
161
|
+
1 if version1 > version2
|
|
162
|
+
"""
|
|
163
|
+
v1 = parse_version(version1)
|
|
164
|
+
v2 = parse_version(version2)
|
|
165
|
+
|
|
166
|
+
if v1 is None or v2 is None:
|
|
167
|
+
return 0 # Can't compare, assume equal
|
|
168
|
+
|
|
169
|
+
if v1 < v2:
|
|
170
|
+
return -1
|
|
171
|
+
elif v1 > v2:
|
|
172
|
+
return 1
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# =============================================================================
|
|
177
|
+
# Tool Detector Classes
|
|
178
|
+
# =============================================================================
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ToolDetector:
|
|
182
|
+
"""Base class for detecting tools in the environment."""
|
|
183
|
+
|
|
184
|
+
# Tools this detector handles
|
|
185
|
+
supported_tools: list[str] = []
|
|
186
|
+
|
|
187
|
+
# Version command arguments by tool
|
|
188
|
+
version_args: dict[str, list[str]] = {
|
|
189
|
+
"default": ["--version"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def detect_tool(self, tool_name: str) -> ToolInfo:
|
|
193
|
+
"""Detect a tool and get its information.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
tool_name: Name of the tool to detect
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
ToolInfo with detection results
|
|
200
|
+
"""
|
|
201
|
+
path = shutil.which(tool_name)
|
|
202
|
+
|
|
203
|
+
if path is None:
|
|
204
|
+
return ToolInfo(
|
|
205
|
+
name=tool_name,
|
|
206
|
+
path=None,
|
|
207
|
+
version=None,
|
|
208
|
+
status=ToolStatus.NOT_FOUND,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Get version
|
|
212
|
+
version_args = self.version_args.get(tool_name, self.version_args["default"])
|
|
213
|
+
version = self.get_version(path, version_args)
|
|
214
|
+
|
|
215
|
+
# Check version compatibility
|
|
216
|
+
min_version = MIN_VERSIONS.get(tool_name)
|
|
217
|
+
if min_version and version:
|
|
218
|
+
if compare_versions(version, min_version) < 0:
|
|
219
|
+
return ToolInfo(
|
|
220
|
+
name=tool_name,
|
|
221
|
+
path=path,
|
|
222
|
+
version=version,
|
|
223
|
+
status=ToolStatus.VERSION_INCOMPATIBLE,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return ToolInfo(
|
|
227
|
+
name=tool_name,
|
|
228
|
+
path=path,
|
|
229
|
+
version=version,
|
|
230
|
+
status=ToolStatus.AVAILABLE,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def get_version(self, tool_path: str, version_args: list[str]) -> Optional[str]:
|
|
234
|
+
"""Get the version of a tool.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
tool_path: Path to the tool executable
|
|
238
|
+
version_args: Arguments to get version (e.g., ["--version"])
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Version string or None if extraction fails
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
result = subprocess.run(
|
|
245
|
+
[tool_path] + version_args,
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
timeout=10,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
output = result.stdout or result.stderr
|
|
252
|
+
|
|
253
|
+
# Extract version from output
|
|
254
|
+
parsed = parse_version(output)
|
|
255
|
+
if parsed:
|
|
256
|
+
return f"{parsed[0]}.{parsed[1]}.{parsed[2]}"
|
|
257
|
+
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
except (subprocess.SubprocessError, OSError) as e:
|
|
261
|
+
logger.debug(f"Failed to get version for {tool_path}: {e}")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def check_version_compatibility(self, version: str, min_version: str) -> bool:
|
|
265
|
+
"""Check if a version meets the minimum requirement.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
version: Detected version
|
|
269
|
+
min_version: Minimum required version
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if version >= min_version
|
|
273
|
+
"""
|
|
274
|
+
return compare_versions(version, min_version) >= 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class PythonToolDetector(ToolDetector):
|
|
278
|
+
"""Detector for Python ecosystem tools."""
|
|
279
|
+
|
|
280
|
+
supported_tools = [
|
|
281
|
+
"python",
|
|
282
|
+
"python3",
|
|
283
|
+
"pytest",
|
|
284
|
+
"ruff",
|
|
285
|
+
"mypy",
|
|
286
|
+
"black",
|
|
287
|
+
"uv",
|
|
288
|
+
"pip",
|
|
289
|
+
"poetry",
|
|
290
|
+
"pipenv",
|
|
291
|
+
"pyright",
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
version_args = {
|
|
295
|
+
"default": ["--version"],
|
|
296
|
+
"python": ["--version"],
|
|
297
|
+
"python3": ["--version"],
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class JavaScriptToolDetector(ToolDetector):
|
|
302
|
+
"""Detector for JavaScript ecosystem tools."""
|
|
303
|
+
|
|
304
|
+
supported_tools = [
|
|
305
|
+
"node",
|
|
306
|
+
"npm",
|
|
307
|
+
"pnpm",
|
|
308
|
+
"yarn",
|
|
309
|
+
"jest",
|
|
310
|
+
"eslint",
|
|
311
|
+
"prettier",
|
|
312
|
+
"webpack",
|
|
313
|
+
"vite",
|
|
314
|
+
"bun",
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
version_args = {
|
|
318
|
+
"default": ["--version"],
|
|
319
|
+
"node": ["--version"],
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class RustToolDetector(ToolDetector):
|
|
324
|
+
"""Detector for Rust ecosystem tools."""
|
|
325
|
+
|
|
326
|
+
supported_tools = [
|
|
327
|
+
"cargo",
|
|
328
|
+
"rustc",
|
|
329
|
+
"clippy",
|
|
330
|
+
"rustfmt",
|
|
331
|
+
"rust-analyzer",
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
version_args = {
|
|
335
|
+
"default": ["--version"],
|
|
336
|
+
"cargo": ["--version"],
|
|
337
|
+
"rustc": ["--version"],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class GenericToolDetector(ToolDetector):
|
|
342
|
+
"""Detector for generic tools (git, docker, etc.)."""
|
|
343
|
+
|
|
344
|
+
supported_tools = [
|
|
345
|
+
"git",
|
|
346
|
+
"docker",
|
|
347
|
+
"make",
|
|
348
|
+
"curl",
|
|
349
|
+
"wget",
|
|
350
|
+
"jq",
|
|
351
|
+
"gh",
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
version_args = {
|
|
355
|
+
"default": ["--version"],
|
|
356
|
+
"git": ["--version"],
|
|
357
|
+
"docker": ["--version"],
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# =============================================================================
|
|
362
|
+
# Project Type Detector
|
|
363
|
+
# =============================================================================
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class ProjectTypeDetector:
|
|
367
|
+
"""Detects project type from manifest files."""
|
|
368
|
+
|
|
369
|
+
# Priority order for detection (first match wins)
|
|
370
|
+
PROJECT_MARKERS = [
|
|
371
|
+
("pyproject.toml", "python"),
|
|
372
|
+
("setup.py", "python"),
|
|
373
|
+
("requirements.txt", "python"),
|
|
374
|
+
("package.json", "javascript"),
|
|
375
|
+
("Cargo.toml", "rust"),
|
|
376
|
+
("go.mod", "go"),
|
|
377
|
+
("pom.xml", "java"),
|
|
378
|
+
("build.gradle", "java"),
|
|
379
|
+
("Gemfile", "ruby"),
|
|
380
|
+
("composer.json", "php"),
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
# Required tools by project type
|
|
384
|
+
REQUIRED_TOOLS = {
|
|
385
|
+
"python": ["python", "pip"],
|
|
386
|
+
"javascript": ["node", "npm"],
|
|
387
|
+
"rust": ["cargo", "rustc"],
|
|
388
|
+
"go": ["go"],
|
|
389
|
+
"java": ["java", "javac"],
|
|
390
|
+
"ruby": ["ruby", "gem"],
|
|
391
|
+
"php": ["php", "composer"],
|
|
392
|
+
"unknown": ["git"],
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# Optional tools by project type
|
|
396
|
+
OPTIONAL_TOOLS = {
|
|
397
|
+
"python": ["pytest", "ruff", "mypy", "black", "uv"],
|
|
398
|
+
"javascript": ["jest", "eslint", "prettier"],
|
|
399
|
+
"rust": ["clippy", "rustfmt"],
|
|
400
|
+
"go": [],
|
|
401
|
+
"java": ["mvn", "gradle"],
|
|
402
|
+
"ruby": ["bundler", "rspec"],
|
|
403
|
+
"php": [],
|
|
404
|
+
"unknown": [],
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def detect_project_type(self, project_dir: Path) -> str:
|
|
408
|
+
"""Detect the project type from manifest files.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
project_dir: Path to project directory
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Project type string (e.g., "python", "javascript", "unknown")
|
|
415
|
+
"""
|
|
416
|
+
for marker_file, project_type in self.PROJECT_MARKERS:
|
|
417
|
+
if (project_dir / marker_file).exists():
|
|
418
|
+
logger.debug(f"Detected {project_type} project from {marker_file}")
|
|
419
|
+
return project_type
|
|
420
|
+
|
|
421
|
+
logger.debug("Could not detect project type, returning 'unknown'")
|
|
422
|
+
return "unknown"
|
|
423
|
+
|
|
424
|
+
def get_required_tools(self, project_type: str) -> list[str]:
|
|
425
|
+
"""Get required tools for a project type.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
project_type: Project type string
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
List of required tool names
|
|
432
|
+
"""
|
|
433
|
+
return self.REQUIRED_TOOLS.get(project_type, self.REQUIRED_TOOLS["unknown"])
|
|
434
|
+
|
|
435
|
+
def get_optional_tools(self, project_type: str) -> list[str]:
|
|
436
|
+
"""Get optional tools for a project type.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
project_type: Project type string
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List of optional tool names
|
|
443
|
+
"""
|
|
444
|
+
return self.OPTIONAL_TOOLS.get(project_type, [])
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# =============================================================================
|
|
448
|
+
# Environment Validator
|
|
449
|
+
# =============================================================================
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class EnvironmentValidator:
|
|
453
|
+
"""Validates the development environment for a project."""
|
|
454
|
+
|
|
455
|
+
def __init__(self):
|
|
456
|
+
"""Initialize the validator with all detectors."""
|
|
457
|
+
self.project_detector = ProjectTypeDetector()
|
|
458
|
+
self.detectors = {
|
|
459
|
+
"python": PythonToolDetector(),
|
|
460
|
+
"javascript": JavaScriptToolDetector(),
|
|
461
|
+
"rust": RustToolDetector(),
|
|
462
|
+
"generic": GenericToolDetector(),
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
def validate_environment(
|
|
466
|
+
self,
|
|
467
|
+
project_dir: Path,
|
|
468
|
+
required_tools: Optional[list[str]] = None,
|
|
469
|
+
optional_tools: Optional[list[str]] = None,
|
|
470
|
+
) -> ValidationResult:
|
|
471
|
+
"""Validate the environment for a project.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
project_dir: Path to project directory
|
|
475
|
+
required_tools: Override list of required tools
|
|
476
|
+
optional_tools: Override list of optional tools
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
ValidationResult with detection results and recommendations
|
|
480
|
+
"""
|
|
481
|
+
# Detect project type
|
|
482
|
+
project_type = self.project_detector.detect_project_type(project_dir)
|
|
483
|
+
|
|
484
|
+
# Determine required and optional tools
|
|
485
|
+
if required_tools is None:
|
|
486
|
+
required_tools = self.project_detector.get_required_tools(project_type)
|
|
487
|
+
if optional_tools is None:
|
|
488
|
+
optional_tools = self.project_detector.get_optional_tools(project_type)
|
|
489
|
+
|
|
490
|
+
# Always require git
|
|
491
|
+
if "git" not in required_tools:
|
|
492
|
+
required_tools = ["git"] + list(required_tools)
|
|
493
|
+
|
|
494
|
+
# Detect all tools
|
|
495
|
+
all_tools = list(set(required_tools + optional_tools))
|
|
496
|
+
detected_tools = self._detect_all_tools(all_tools)
|
|
497
|
+
|
|
498
|
+
# Find missing tools
|
|
499
|
+
missing_tools = [
|
|
500
|
+
tool for tool in required_tools
|
|
501
|
+
if tool not in detected_tools or not detected_tools[tool].is_available
|
|
502
|
+
]
|
|
503
|
+
optional_missing = [
|
|
504
|
+
tool for tool in optional_tools
|
|
505
|
+
if tool not in detected_tools or not detected_tools[tool].is_available
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
# Calculate health score
|
|
509
|
+
health_score = self.calculate_health_score(detected_tools, required_tools)
|
|
510
|
+
|
|
511
|
+
# Generate recommendations
|
|
512
|
+
recommendations = self.generate_recommendations(detected_tools, project_type)
|
|
513
|
+
|
|
514
|
+
# Generate warnings
|
|
515
|
+
warnings = self._generate_warnings(detected_tools, project_type)
|
|
516
|
+
|
|
517
|
+
# Detect conflicts
|
|
518
|
+
conflicts = self._detect_conflicts(detected_tools)
|
|
519
|
+
|
|
520
|
+
return ValidationResult(
|
|
521
|
+
project_type=project_type,
|
|
522
|
+
detected_tools=detected_tools,
|
|
523
|
+
missing_tools=missing_tools,
|
|
524
|
+
optional_missing=optional_missing,
|
|
525
|
+
health_score=health_score,
|
|
526
|
+
recommendations=recommendations,
|
|
527
|
+
warnings=warnings,
|
|
528
|
+
conflicts=conflicts,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def _detect_all_tools(self, tools: list[str]) -> dict[str, ToolInfo]:
|
|
532
|
+
"""Detect all specified tools.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
tools: List of tool names to detect
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Dictionary mapping tool names to ToolInfo
|
|
539
|
+
"""
|
|
540
|
+
results = {}
|
|
541
|
+
for tool in tools:
|
|
542
|
+
detector = self._get_detector_for_tool(tool)
|
|
543
|
+
results[tool] = detector.detect_tool(tool)
|
|
544
|
+
return results
|
|
545
|
+
|
|
546
|
+
def _get_detector_for_tool(self, tool_name: str) -> ToolDetector:
|
|
547
|
+
"""Get the appropriate detector for a tool.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
tool_name: Name of the tool
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Appropriate ToolDetector instance
|
|
554
|
+
"""
|
|
555
|
+
for detector in self.detectors.values():
|
|
556
|
+
if tool_name in detector.supported_tools:
|
|
557
|
+
return detector
|
|
558
|
+
return self.detectors["generic"]
|
|
559
|
+
|
|
560
|
+
def calculate_health_score(
|
|
561
|
+
self,
|
|
562
|
+
detected_tools: dict[str, ToolInfo],
|
|
563
|
+
required: list[str],
|
|
564
|
+
) -> float:
|
|
565
|
+
"""Calculate environment health score.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
detected_tools: Dictionary of detected tools
|
|
569
|
+
required: List of required tool names
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Health score from 0.0 to 1.0
|
|
573
|
+
"""
|
|
574
|
+
if not required:
|
|
575
|
+
return 1.0
|
|
576
|
+
|
|
577
|
+
available_count = sum(
|
|
578
|
+
1 for tool in required
|
|
579
|
+
if tool in detected_tools and detected_tools[tool].is_available
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return available_count / len(required)
|
|
583
|
+
|
|
584
|
+
def generate_recommendations(
|
|
585
|
+
self,
|
|
586
|
+
detected_tools: dict[str, ToolInfo],
|
|
587
|
+
project_type: str,
|
|
588
|
+
) -> list[str]:
|
|
589
|
+
"""Generate recommendations for improving the environment.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
detected_tools: Dictionary of detected tools
|
|
593
|
+
project_type: Type of project
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
List of recommendation strings
|
|
597
|
+
"""
|
|
598
|
+
recommendations = []
|
|
599
|
+
|
|
600
|
+
for tool_name, tool_info in detected_tools.items():
|
|
601
|
+
if tool_info.status == ToolStatus.NOT_FOUND:
|
|
602
|
+
install_cmd = self._get_install_command(tool_name, project_type)
|
|
603
|
+
recommendations.append(
|
|
604
|
+
f"Install {tool_name}: {install_cmd}"
|
|
605
|
+
)
|
|
606
|
+
elif tool_info.status == ToolStatus.VERSION_INCOMPATIBLE:
|
|
607
|
+
min_ver = MIN_VERSIONS.get(tool_name, "latest")
|
|
608
|
+
recommendations.append(
|
|
609
|
+
f"Upgrade {tool_name} to version {min_ver} or higher "
|
|
610
|
+
f"(current: {tool_info.version})"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
return recommendations
|
|
614
|
+
|
|
615
|
+
def _generate_warnings(
|
|
616
|
+
self,
|
|
617
|
+
detected_tools: dict[str, ToolInfo],
|
|
618
|
+
project_type: str,
|
|
619
|
+
) -> list[str]:
|
|
620
|
+
"""Generate warnings about the environment.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
detected_tools: Dictionary of detected tools
|
|
624
|
+
project_type: Type of project
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
List of warning strings
|
|
628
|
+
"""
|
|
629
|
+
warnings = []
|
|
630
|
+
|
|
631
|
+
# Check for critical missing tools
|
|
632
|
+
critical_missing = [
|
|
633
|
+
tool for tool, info in detected_tools.items()
|
|
634
|
+
if not info.is_available and tool in ["git", "python", "node", "cargo"]
|
|
635
|
+
]
|
|
636
|
+
if critical_missing:
|
|
637
|
+
warnings.append(
|
|
638
|
+
f"Critical tools missing: {', '.join(critical_missing)}"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
return warnings
|
|
642
|
+
|
|
643
|
+
def _detect_conflicts(self, detected_tools: dict[str, ToolInfo]) -> list[str]:
|
|
644
|
+
"""Detect tool conflicts.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
detected_tools: Dictionary of detected tools
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
List of conflict descriptions
|
|
651
|
+
"""
|
|
652
|
+
conflicts = []
|
|
653
|
+
|
|
654
|
+
# Example: Check for conflicting Python versions
|
|
655
|
+
# (Could be expanded based on real-world conflicts)
|
|
656
|
+
|
|
657
|
+
return conflicts
|
|
658
|
+
|
|
659
|
+
def _get_install_command(self, tool_name: str, project_type: str) -> str:
|
|
660
|
+
"""Get install command for a tool.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
tool_name: Name of the tool
|
|
664
|
+
project_type: Type of project
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Install command string
|
|
668
|
+
"""
|
|
669
|
+
# Python tools
|
|
670
|
+
if tool_name in ["pytest", "ruff", "mypy", "black"]:
|
|
671
|
+
return f"pip install {tool_name}"
|
|
672
|
+
if tool_name == "uv":
|
|
673
|
+
return "curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
674
|
+
if tool_name == "pip":
|
|
675
|
+
return "python -m ensurepip --upgrade"
|
|
676
|
+
|
|
677
|
+
# JavaScript tools
|
|
678
|
+
if tool_name in ["jest", "eslint", "prettier"]:
|
|
679
|
+
return f"npm install -g {tool_name}"
|
|
680
|
+
if tool_name == "node":
|
|
681
|
+
return "See https://nodejs.org/ or use nvm"
|
|
682
|
+
if tool_name == "npm":
|
|
683
|
+
return "Installed with Node.js"
|
|
684
|
+
|
|
685
|
+
# Rust tools
|
|
686
|
+
if tool_name in ["cargo", "rustc"]:
|
|
687
|
+
return "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
|
688
|
+
if tool_name in ["clippy", "rustfmt"]:
|
|
689
|
+
return f"rustup component add {tool_name}"
|
|
690
|
+
|
|
691
|
+
# Generic tools
|
|
692
|
+
if tool_name == "git":
|
|
693
|
+
return "apt install git / brew install git"
|
|
694
|
+
if tool_name == "docker":
|
|
695
|
+
return "See https://docs.docker.com/get-docker/"
|
|
696
|
+
|
|
697
|
+
return f"Install {tool_name} using your system package manager"
|