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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. 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"