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,650 @@
1
+ """Tool installation manager for CodeFRAME.
2
+
3
+ This module provides:
4
+ - Platform-specific tool installation
5
+ - Installation verification
6
+ - Installation history tracking
7
+ - Rollback capabilities
8
+
9
+ Usage:
10
+ from codeframe.core.installer import ToolInstaller
11
+
12
+ installer = ToolInstaller()
13
+ if installer.can_install("pytest"):
14
+ result = installer.install_tool("pytest", confirm=True)
15
+ if result.success:
16
+ print(f"Installed {result.tool_name}")
17
+ """
18
+
19
+ import json
20
+ import logging
21
+ import platform
22
+ import shutil
23
+ import subprocess
24
+ from dataclasses import dataclass
25
+ from datetime import datetime, UTC
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
+ # Constants
35
+ # =============================================================================
36
+
37
+
38
+ def get_platform() -> str:
39
+ """Get the current platform.
40
+
41
+ Returns:
42
+ 'linux', 'darwin', or 'windows'
43
+ """
44
+ system = platform.system()
45
+ if system == "Linux":
46
+ return "linux"
47
+ elif system == "Darwin":
48
+ return "darwin"
49
+ elif system == "Windows":
50
+ return "windows"
51
+ logger.warning(f"Unknown platform '{system}', defaulting to 'linux'")
52
+ return "linux"
53
+
54
+
55
+ # =============================================================================
56
+ # Enums and Data Classes
57
+ # =============================================================================
58
+
59
+
60
+ class InstallStatus(str, Enum):
61
+ """Status of an installation attempt."""
62
+
63
+ SUCCESS = "success"
64
+ FAILED = "failed"
65
+ SKIPPED = "skipped"
66
+ CANCELLED = "cancelled"
67
+
68
+
69
+ @dataclass
70
+ class InstallResult:
71
+ """Result of an installation attempt."""
72
+
73
+ tool_name: str
74
+ status: InstallStatus
75
+ message: str
76
+ command_used: Optional[str] = None
77
+ error_output: Optional[str] = None
78
+ installed_version: Optional[str] = None
79
+
80
+ @property
81
+ def success(self) -> bool:
82
+ """Check if installation was successful or skipped."""
83
+ return self.status in (InstallStatus.SUCCESS, InstallStatus.SKIPPED)
84
+
85
+
86
+ # =============================================================================
87
+ # Sub-Installers
88
+ # =============================================================================
89
+
90
+
91
+ class PipInstaller:
92
+ """Installer for Python packages via pip/uv."""
93
+
94
+ SUPPORTED_TOOLS = {
95
+ "pytest",
96
+ "ruff",
97
+ "mypy",
98
+ "black",
99
+ "flake8",
100
+ "pylint",
101
+ "isort",
102
+ "bandit",
103
+ "coverage",
104
+ "pre-commit",
105
+ "httpx",
106
+ "requests",
107
+ }
108
+
109
+ def can_install(self, tool_name: str) -> bool:
110
+ """Check if this installer can install the tool."""
111
+ return tool_name in self.SUPPORTED_TOOLS
112
+
113
+ def get_install_command(self, tool_name: str) -> str:
114
+ """Get the install command for a tool (display string).
115
+
116
+ Prefers uv if available, falls back to pip.
117
+ """
118
+ if shutil.which("uv"):
119
+ return f"uv pip install {tool_name}"
120
+ return f"pip install {tool_name}"
121
+
122
+ def _get_install_cmd_parts(self, tool_name: str) -> list[str]:
123
+ """Get the install command as a list of arguments.
124
+
125
+ Args:
126
+ tool_name: Name of the package to install
127
+
128
+ Returns:
129
+ List of command arguments for subprocess
130
+ """
131
+ if shutil.which("uv"):
132
+ return ["uv", "pip", "install", tool_name]
133
+ return ["pip", "install", tool_name]
134
+
135
+ def install_tool(
136
+ self,
137
+ tool_name: str,
138
+ confirm: bool = True,
139
+ force: bool = False,
140
+ ) -> InstallResult:
141
+ """Install a Python package.
142
+
143
+ Args:
144
+ tool_name: Name of the package to install
145
+ confirm: Whether to prompt for confirmation
146
+ force: Reinstall even if already installed
147
+
148
+ Returns:
149
+ InstallResult with status and details
150
+ """
151
+ # Check if already installed
152
+ if not force and shutil.which(tool_name):
153
+ return InstallResult(
154
+ tool_name=tool_name,
155
+ status=InstallStatus.SKIPPED,
156
+ message=f"{tool_name} is already installed",
157
+ )
158
+
159
+ command = self.get_install_command(tool_name)
160
+ cmd_parts = self._get_install_cmd_parts(tool_name)
161
+
162
+ try:
163
+ result = subprocess.run(
164
+ cmd_parts,
165
+ capture_output=True,
166
+ text=True,
167
+ timeout=300, # 5 minute timeout
168
+ )
169
+
170
+ if result.returncode == 0:
171
+ return InstallResult(
172
+ tool_name=tool_name,
173
+ status=InstallStatus.SUCCESS,
174
+ message=f"Successfully installed {tool_name}",
175
+ command_used=command,
176
+ )
177
+ else:
178
+ return InstallResult(
179
+ tool_name=tool_name,
180
+ status=InstallStatus.FAILED,
181
+ message=f"Failed to install {tool_name}",
182
+ command_used=command,
183
+ error_output=result.stderr,
184
+ )
185
+
186
+ except subprocess.TimeoutExpired:
187
+ return InstallResult(
188
+ tool_name=tool_name,
189
+ status=InstallStatus.FAILED,
190
+ message=f"Installation of {tool_name} timed out",
191
+ command_used=command,
192
+ )
193
+ except Exception as e:
194
+ return InstallResult(
195
+ tool_name=tool_name,
196
+ status=InstallStatus.FAILED,
197
+ message=f"Installation error: {e}",
198
+ command_used=command,
199
+ error_output=str(e),
200
+ )
201
+
202
+
203
+ class NpmInstaller:
204
+ """Installer for JavaScript packages via npm."""
205
+
206
+ SUPPORTED_TOOLS = {
207
+ "eslint",
208
+ "prettier",
209
+ "jest",
210
+ "typescript",
211
+ "ts-node",
212
+ "webpack",
213
+ "vite",
214
+ "vitest",
215
+ "mocha",
216
+ "chai",
217
+ }
218
+
219
+ def can_install(self, tool_name: str) -> bool:
220
+ """Check if this installer can install the tool."""
221
+ return tool_name in self.SUPPORTED_TOOLS
222
+
223
+ def get_install_command(self, tool_name: str, global_install: bool = True) -> str:
224
+ """Get the install command for a tool (display string).
225
+
226
+ Args:
227
+ tool_name: Name of the package
228
+ global_install: Install globally (-g) or as dev dependency (-D)
229
+ """
230
+ if global_install:
231
+ return f"npm install -g {tool_name}"
232
+ return f"npm install -D {tool_name}"
233
+
234
+ def _get_install_cmd_parts(
235
+ self, tool_name: str, global_install: bool = True
236
+ ) -> list[str]:
237
+ """Get the install command as a list of arguments.
238
+
239
+ Args:
240
+ tool_name: Name of the package
241
+ global_install: Install globally (-g) or as dev dependency (-D)
242
+
243
+ Returns:
244
+ List of command arguments for subprocess
245
+ """
246
+ if global_install:
247
+ return ["npm", "install", "-g", tool_name]
248
+ return ["npm", "install", "-D", tool_name]
249
+
250
+ def install_tool(
251
+ self,
252
+ tool_name: str,
253
+ confirm: bool = True,
254
+ force: bool = False,
255
+ global_install: bool = True,
256
+ ) -> InstallResult:
257
+ """Install a JavaScript package.
258
+
259
+ Args:
260
+ tool_name: Name of the package to install
261
+ confirm: Whether to prompt for confirmation
262
+ force: Reinstall even if already installed
263
+ global_install: Install globally or locally
264
+
265
+ Returns:
266
+ InstallResult with status and details
267
+ """
268
+ # Check if already installed (only for global installs)
269
+ if global_install and not force and shutil.which(tool_name):
270
+ return InstallResult(
271
+ tool_name=tool_name,
272
+ status=InstallStatus.SKIPPED,
273
+ message=f"{tool_name} is already installed",
274
+ )
275
+
276
+ command = self.get_install_command(tool_name, global_install)
277
+ cmd_parts = self._get_install_cmd_parts(tool_name, global_install)
278
+
279
+ try:
280
+ result = subprocess.run(
281
+ cmd_parts,
282
+ capture_output=True,
283
+ text=True,
284
+ timeout=300,
285
+ )
286
+
287
+ if result.returncode == 0:
288
+ return InstallResult(
289
+ tool_name=tool_name,
290
+ status=InstallStatus.SUCCESS,
291
+ message=f"Successfully installed {tool_name}",
292
+ command_used=command,
293
+ )
294
+ else:
295
+ return InstallResult(
296
+ tool_name=tool_name,
297
+ status=InstallStatus.FAILED,
298
+ message=f"Failed to install {tool_name}",
299
+ command_used=command,
300
+ error_output=result.stderr,
301
+ )
302
+
303
+ except Exception as e:
304
+ return InstallResult(
305
+ tool_name=tool_name,
306
+ status=InstallStatus.FAILED,
307
+ message=f"Installation error: {e}",
308
+ command_used=command,
309
+ error_output=str(e),
310
+ )
311
+
312
+
313
+ class CargoInstaller:
314
+ """Installer for Rust tools via cargo/rustup."""
315
+
316
+ # Tools installed via rustup component add
317
+ RUSTUP_COMPONENTS = {
318
+ "clippy",
319
+ "rustfmt",
320
+ "rust-analyzer",
321
+ "rust-src",
322
+ }
323
+
324
+ # Tools installed via cargo install
325
+ CARGO_PACKAGES = {
326
+ "ripgrep",
327
+ "fd-find",
328
+ "bat",
329
+ "tokei",
330
+ "cargo-edit",
331
+ "cargo-watch",
332
+ }
333
+
334
+ # Mapping from package name to binary name (when different)
335
+ BINARY_NAMES: dict[str, str] = {
336
+ "ripgrep": "rg",
337
+ "fd-find": "fd",
338
+ "clippy": "cargo-clippy",
339
+ "cargo-edit": "cargo-add",
340
+ }
341
+
342
+ def can_install(self, tool_name: str) -> bool:
343
+ """Check if this installer can install the tool."""
344
+ return tool_name in self.RUSTUP_COMPONENTS or tool_name in self.CARGO_PACKAGES
345
+
346
+ def _get_binary_name(self, tool_name: str) -> str:
347
+ """Get the binary name for a tool (may differ from package name)."""
348
+ return self.BINARY_NAMES.get(tool_name, tool_name)
349
+
350
+ def get_install_command(self, tool_name: str) -> str:
351
+ """Get the install command for a tool (display string)."""
352
+ if tool_name in self.RUSTUP_COMPONENTS:
353
+ return f"rustup component add {tool_name}"
354
+ return f"cargo install {tool_name}"
355
+
356
+ def _get_install_cmd_parts(self, tool_name: str) -> list[str]:
357
+ """Get the install command as a list of arguments.
358
+
359
+ Args:
360
+ tool_name: Name of the tool to install
361
+
362
+ Returns:
363
+ List of command arguments for subprocess
364
+ """
365
+ if tool_name in self.RUSTUP_COMPONENTS:
366
+ return ["rustup", "component", "add", tool_name]
367
+ return ["cargo", "install", tool_name]
368
+
369
+ def install_tool(
370
+ self,
371
+ tool_name: str,
372
+ confirm: bool = True,
373
+ force: bool = False,
374
+ ) -> InstallResult:
375
+ """Install a Rust tool.
376
+
377
+ Args:
378
+ tool_name: Name of the tool to install
379
+ confirm: Whether to prompt for confirmation
380
+ force: Reinstall even if already installed
381
+
382
+ Returns:
383
+ InstallResult with status and details
384
+ """
385
+ # Check if already installed (use binary name which may differ from package)
386
+ # Note: rust-src has no binary, skip check for it
387
+ if not force and tool_name != "rust-src":
388
+ binary_name = self._get_binary_name(tool_name)
389
+ if shutil.which(binary_name):
390
+ return InstallResult(
391
+ tool_name=tool_name,
392
+ status=InstallStatus.SKIPPED,
393
+ message=f"{tool_name} is already installed",
394
+ )
395
+
396
+ command = self.get_install_command(tool_name)
397
+ cmd_parts = self._get_install_cmd_parts(tool_name)
398
+
399
+ try:
400
+ result = subprocess.run(
401
+ cmd_parts,
402
+ capture_output=True,
403
+ text=True,
404
+ timeout=600, # Cargo builds can take a while
405
+ )
406
+
407
+ if result.returncode == 0:
408
+ return InstallResult(
409
+ tool_name=tool_name,
410
+ status=InstallStatus.SUCCESS,
411
+ message=f"Successfully installed {tool_name}",
412
+ command_used=command,
413
+ )
414
+ else:
415
+ return InstallResult(
416
+ tool_name=tool_name,
417
+ status=InstallStatus.FAILED,
418
+ message=f"Failed to install {tool_name}",
419
+ command_used=command,
420
+ error_output=result.stderr,
421
+ )
422
+
423
+ except Exception as e:
424
+ return InstallResult(
425
+ tool_name=tool_name,
426
+ status=InstallStatus.FAILED,
427
+ message=f"Installation error: {e}",
428
+ command_used=command,
429
+ error_output=str(e),
430
+ )
431
+
432
+
433
+ class SystemInstaller:
434
+ """Installer for system packages."""
435
+
436
+ SUPPORTED_TOOLS = {
437
+ "git",
438
+ "docker",
439
+ "make",
440
+ "curl",
441
+ "wget",
442
+ "jq",
443
+ "gh",
444
+ }
445
+
446
+ def can_install(self, tool_name: str) -> bool:
447
+ """Check if this installer can install the tool."""
448
+ return tool_name in self.SUPPORTED_TOOLS
449
+
450
+ def get_install_command(self, tool_name: str) -> str:
451
+ """Get the install command for a tool based on platform."""
452
+ platform = get_platform()
453
+
454
+ if platform == "darwin":
455
+ return f"brew install {tool_name}"
456
+ elif platform == "windows":
457
+ return f"choco install {tool_name}"
458
+ else: # linux
459
+ return f"sudo apt install -y {tool_name}"
460
+
461
+ def install_tool(
462
+ self,
463
+ tool_name: str,
464
+ confirm: bool = True,
465
+ force: bool = False,
466
+ ) -> InstallResult:
467
+ """Install a system tool.
468
+
469
+ Note: System installations typically require elevated privileges.
470
+
471
+ Args:
472
+ tool_name: Name of the tool to install
473
+ confirm: Whether to prompt for confirmation
474
+ force: Reinstall even if already installed
475
+
476
+ Returns:
477
+ InstallResult with status and details
478
+ """
479
+ command = self.get_install_command(tool_name)
480
+
481
+ # For system tools, we return the command but don't execute
482
+ # automatically as it requires elevated privileges
483
+ return InstallResult(
484
+ tool_name=tool_name,
485
+ status=InstallStatus.CANCELLED,
486
+ message=f"System installation requires manual execution: {command}",
487
+ command_used=command,
488
+ )
489
+
490
+
491
+ # =============================================================================
492
+ # Main Tool Installer
493
+ # =============================================================================
494
+
495
+
496
+ class ToolInstaller:
497
+ """Main installer that delegates to specialized sub-installers."""
498
+
499
+ def __init__(self, history_dir: Optional[Path] = None):
500
+ """Initialize the installer.
501
+
502
+ Args:
503
+ history_dir: Directory to store installation history
504
+ """
505
+ self.history_dir = history_dir or Path(".codeframe")
506
+ self.history_file = self.history_dir / "environment.json"
507
+
508
+ # Initialize sub-installers
509
+ self._pip_installer = PipInstaller()
510
+ self._npm_installer = NpmInstaller()
511
+ self._cargo_installer = CargoInstaller()
512
+ self._system_installer = SystemInstaller()
513
+
514
+ def _get_installer_for_tool(self, tool_name: str):
515
+ """Get the appropriate installer for a tool.
516
+
517
+ Args:
518
+ tool_name: Name of the tool
519
+
520
+ Returns:
521
+ Sub-installer instance or None
522
+ """
523
+ if self._pip_installer.can_install(tool_name):
524
+ return self._pip_installer
525
+ if self._npm_installer.can_install(tool_name):
526
+ return self._npm_installer
527
+ if self._cargo_installer.can_install(tool_name):
528
+ return self._cargo_installer
529
+ if self._system_installer.can_install(tool_name):
530
+ return self._system_installer
531
+ return None
532
+
533
+ def can_install(self, tool_name: str) -> bool:
534
+ """Check if a tool can be installed.
535
+
536
+ Args:
537
+ tool_name: Name of the tool
538
+
539
+ Returns:
540
+ True if installation is supported
541
+ """
542
+ return self._get_installer_for_tool(tool_name) is not None
543
+
544
+ def get_install_command(self, tool_name: str) -> str:
545
+ """Get the install command for a tool.
546
+
547
+ Args:
548
+ tool_name: Name of the tool
549
+
550
+ Returns:
551
+ Install command string or empty string if unsupported
552
+ """
553
+ installer = self._get_installer_for_tool(tool_name)
554
+ if installer:
555
+ return installer.get_install_command(tool_name)
556
+ return ""
557
+
558
+ def verify_installation(self, tool_name: str) -> bool:
559
+ """Verify that a tool is installed.
560
+
561
+ Args:
562
+ tool_name: Name of the tool
563
+
564
+ Returns:
565
+ True if tool is available in PATH
566
+ """
567
+ return shutil.which(tool_name) is not None
568
+
569
+ def install_tool(
570
+ self,
571
+ tool_name: str,
572
+ confirm: bool = True,
573
+ force: bool = False,
574
+ ) -> InstallResult:
575
+ """Install a tool.
576
+
577
+ Args:
578
+ tool_name: Name of the tool to install
579
+ confirm: Whether to prompt for confirmation
580
+ force: Reinstall even if already installed
581
+
582
+ Returns:
583
+ InstallResult with status and details
584
+ """
585
+ installer = self._get_installer_for_tool(tool_name)
586
+ if not installer:
587
+ return InstallResult(
588
+ tool_name=tool_name,
589
+ status=InstallStatus.FAILED,
590
+ message=f"No installer available for {tool_name}",
591
+ )
592
+
593
+ result = installer.install_tool(tool_name, confirm=confirm, force=force)
594
+
595
+ # Record in history
596
+ if result.success:
597
+ self.record_installation(result)
598
+
599
+ return result
600
+
601
+ def record_installation(self, result: InstallResult) -> None:
602
+ """Record an installation in the history file.
603
+
604
+ Args:
605
+ result: Installation result to record
606
+ """
607
+ self.history_dir.mkdir(parents=True, exist_ok=True)
608
+
609
+ # Load existing history
610
+ history = {"installations": {}}
611
+ if self.history_file.exists():
612
+ try:
613
+ with open(self.history_file) as f:
614
+ history = json.load(f)
615
+ except json.JSONDecodeError:
616
+ pass
617
+
618
+ # Add new entry
619
+ history["installations"][result.tool_name] = {
620
+ "status": result.status.value,
621
+ "installed_at": datetime.now(UTC).isoformat(),
622
+ "command": result.command_used,
623
+ "message": result.message,
624
+ }
625
+
626
+ # Save history
627
+ with open(self.history_file, "w") as f:
628
+ json.dump(history, f, indent=2)
629
+
630
+ def get_installation_history(self) -> dict:
631
+ """Get the installation history.
632
+
633
+ Returns:
634
+ Dictionary of tool installations
635
+ """
636
+ if not self.history_file.exists():
637
+ return {}
638
+
639
+ try:
640
+ with open(self.history_file) as f:
641
+ data = json.load(f)
642
+ return data.get("installations", {})
643
+ except (json.JSONDecodeError, IOError):
644
+ return {}
645
+
646
+ def clear_installation_history(self) -> None:
647
+ """Clear the installation history."""
648
+ if self.history_file.exists():
649
+ with open(self.history_file, "w") as f:
650
+ json.dump({"installations": {}}, f)