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,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)
|