code-aide 1.0.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.
code_aide/install.py ADDED
@@ -0,0 +1,305 @@
1
+ """Install implementations for script, npm, and direct-download tools."""
2
+
3
+ import hashlib
4
+ import io
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import tarfile
10
+ from typing import Any, Dict, Optional
11
+
12
+ from code_aide.constants import TOOLS
13
+ from code_aide.console import command_exists, error, info, run_command, success, warning
14
+ from code_aide.versions import fetch_url
15
+
16
+
17
+ def run_install_script(
18
+ install_url: str,
19
+ tool_name: str,
20
+ expected_sha256: Optional[str] = None,
21
+ dryrun: bool = False,
22
+ ) -> bool:
23
+ """Download and run an installation script with SHA256 verification."""
24
+ try:
25
+ info(f"Downloading install script from: {install_url}")
26
+ script_content, _ = fetch_url(install_url)
27
+
28
+ if expected_sha256:
29
+ info("Verifying script integrity with SHA256...")
30
+ actual_sha256 = hashlib.sha256(script_content).hexdigest()
31
+
32
+ if actual_sha256 != expected_sha256:
33
+ error(f"SHA256 verification FAILED for {tool_name}!")
34
+ error(f"Expected: {expected_sha256}")
35
+ error(f"Actual: {actual_sha256}")
36
+ error("")
37
+ error("The install script has changed and needs to be reviewed.")
38
+ error("This could indicate a security issue or a legitimate update.")
39
+ error(
40
+ "Please review the new script and update the SHA256 if it's safe."
41
+ )
42
+ return False
43
+
44
+ success("SHA256 verification passed")
45
+ else:
46
+ warning(
47
+ "No SHA256 checksum configured - script integrity cannot be verified!"
48
+ )
49
+
50
+ if dryrun:
51
+ info(f"[DRYRUN] Would execute install script for {tool_name}")
52
+ return True
53
+
54
+ warning(f"About to execute install script for {tool_name}")
55
+ warning("This will run with your user privileges. Press Ctrl+C to cancel.")
56
+
57
+ bash_process = subprocess.Popen(
58
+ ["bash"],
59
+ stdin=subprocess.PIPE,
60
+ stderr=subprocess.PIPE,
61
+ )
62
+ _, stderr = bash_process.communicate(input=script_content)
63
+
64
+ if bash_process.returncode == 0:
65
+ return True
66
+
67
+ stderr_text = stderr.decode("utf-8", errors="replace")
68
+ error(f"Failed to install {tool_name}: {stderr_text}")
69
+ return False
70
+ except subprocess.CalledProcessError as exc:
71
+ error(f"Failed to download install script for {tool_name}: {exc.stderr}")
72
+ return False
73
+ except Exception as exc:
74
+ error(f"Failed to install {tool_name}: {exc}")
75
+ return False
76
+
77
+
78
+ ARCH_MAP = {
79
+ "x86_64": "x64",
80
+ "amd64": "x64",
81
+ "arm64": "arm64",
82
+ "aarch64": "arm64",
83
+ }
84
+
85
+
86
+ def detect_os_arch() -> tuple:
87
+ """Detect OS and architecture for direct download URLs."""
88
+ os_name = platform.system().lower()
89
+ if os_name not in ("linux", "darwin"):
90
+ raise RuntimeError(f"Unsupported OS: {os_name}")
91
+
92
+ machine = platform.machine()
93
+ arch = ARCH_MAP.get(machine)
94
+ if not arch:
95
+ raise RuntimeError(f"Unsupported architecture: {machine}")
96
+
97
+ return os_name, arch
98
+
99
+
100
+ def extract_tar_member(
101
+ tar_file: tarfile.TarFile, member: tarfile.TarInfo, destination: str
102
+ ) -> None:
103
+ """Extract a tar member using the safest API available."""
104
+ try:
105
+ tar_file.extract(member, destination, filter="data")
106
+ except TypeError:
107
+ tar_file.extract(member, destination)
108
+
109
+
110
+ def install_direct_download(
111
+ tool_name: str,
112
+ tool_config: Dict[str, Any],
113
+ dryrun: bool = False,
114
+ install_dir_override: Optional[str] = None,
115
+ bin_dir_override: Optional[str] = None,
116
+ ) -> bool:
117
+ """Download, extract, and install a tool via direct tarball download."""
118
+ try:
119
+ version = tool_config["latest_version"]
120
+ install_url = tool_config["install_url"]
121
+ expected_sha256 = tool_config.get("install_sha256")
122
+
123
+ info(f"Verifying install script from: {install_url}")
124
+ script_content, _ = fetch_url(install_url)
125
+
126
+ if expected_sha256:
127
+ actual_sha256 = hashlib.sha256(script_content).hexdigest()
128
+ if actual_sha256 != expected_sha256:
129
+ error(f"SHA256 verification FAILED for {tool_name} install script!")
130
+ error(f"Expected: {expected_sha256}")
131
+ error(f"Actual: {actual_sha256}")
132
+ error("")
133
+ error("The install script has changed and needs to be reviewed.")
134
+ return False
135
+ success("Install script SHA256 verified")
136
+
137
+ os_name, arch = detect_os_arch()
138
+ info(f"Platform: {os_name}/{arch}")
139
+
140
+ url_template = tool_config["download_url_template"]
141
+ download_url = url_template.format(version=version, os=os_name, arch=arch)
142
+ info(f"Download URL: {download_url}")
143
+
144
+ install_dir_template = tool_config["install_dir"]
145
+ install_dir = os.path.expanduser(install_dir_template.format(version=version))
146
+ bin_dir = os.path.expanduser(tool_config["bin_dir"])
147
+
148
+ if install_dir_override:
149
+ install_dir = install_dir_override
150
+ if bin_dir_override:
151
+ bin_dir = bin_dir_override
152
+
153
+ if dryrun:
154
+ info(f"[DRYRUN] Would download: {download_url}")
155
+ info(f"[DRYRUN] Would extract to: {install_dir}")
156
+ info(f"[DRYRUN] Would create symlinks in: {bin_dir}")
157
+ for link_name, target in tool_config.get("symlinks", {}).items():
158
+ info(f"[DRYRUN] {link_name} -> {install_dir}/{target}")
159
+ return True
160
+
161
+ info("Downloading package...")
162
+ tarball_data, _ = fetch_url(download_url, timeout=120)
163
+ success(f"Downloaded {len(tarball_data)} bytes")
164
+
165
+ temp_dir = install_dir + f".tmp-{os.getpid()}"
166
+ os.makedirs(temp_dir, exist_ok=True)
167
+
168
+ try:
169
+ with tarfile.open(fileobj=io.BytesIO(tarball_data), mode="r:gz") as tf:
170
+ for member in tf.getmembers():
171
+ parts = member.name.split("/", 1)
172
+ if len(parts) <= 1 and not member.isdir():
173
+ continue
174
+ if len(parts) > 1:
175
+ member_name = parts[1]
176
+ else:
177
+ continue
178
+
179
+ normalized_name = os.path.normpath(member_name)
180
+ if normalized_name in ("", "."):
181
+ continue
182
+ if normalized_name.startswith("..") or os.path.isabs(
183
+ normalized_name
184
+ ):
185
+ continue
186
+
187
+ member.name = normalized_name
188
+ extract_tar_member(tf, member, temp_dir)
189
+
190
+ if os.path.exists(install_dir):
191
+ shutil.rmtree(install_dir)
192
+ os.rename(temp_dir, install_dir)
193
+ success(f"Extracted to {install_dir}")
194
+
195
+ except Exception:
196
+ if os.path.exists(temp_dir):
197
+ shutil.rmtree(temp_dir)
198
+ raise
199
+
200
+ os.makedirs(bin_dir, exist_ok=True)
201
+ symlinks = tool_config.get("symlinks", {})
202
+ for link_name, target_name in symlinks.items():
203
+ link_path = os.path.join(bin_dir, link_name)
204
+ target_path = os.path.join(install_dir, target_name)
205
+
206
+ if os.path.lexists(link_path):
207
+ os.remove(link_path)
208
+
209
+ os.symlink(target_path, link_path)
210
+ info(f"Symlink: {link_path} -> {target_path}")
211
+
212
+ success(f"{tool_config['name']} installed successfully")
213
+ return True
214
+
215
+ except RuntimeError as exc:
216
+ error(str(exc))
217
+ return False
218
+ except Exception as exc:
219
+ error(f"Failed to install {tool_name}: {exc}")
220
+ return False
221
+
222
+
223
+ def install_tool(tool_name: str, dryrun: bool = False) -> bool:
224
+ """Install a tool based on its configuration."""
225
+ tool_config = TOOLS.get(tool_name)
226
+ if not tool_config:
227
+ error(f"Unknown tool: {tool_name}")
228
+ return False
229
+
230
+ if dryrun:
231
+ info(f"[DRYRUN] Checking {tool_config['name']}...")
232
+ else:
233
+ info(f"Installing {tool_config['name']}...")
234
+
235
+ if command_exists(tool_config["command"]):
236
+ tool_path = shutil.which(tool_config["command"])
237
+ if dryrun:
238
+ info(f"{tool_config['command']} already installed at {tool_path}")
239
+ else:
240
+ warning(f"{tool_config['command']} already installed at {tool_path}")
241
+ return True
242
+
243
+ try:
244
+ install_type = tool_config["install_type"]
245
+
246
+ if install_type == "npm":
247
+ npm_package = tool_config["npm_package"]
248
+ if dryrun:
249
+ info(f"[DRYRUN] Would install npm package: {npm_package}")
250
+ else:
251
+ run_command(["npm", "install", "-g", npm_package], check=True)
252
+ success(f"{tool_config['name']} installed successfully")
253
+ info(tool_config["next_steps"])
254
+ if "docs_url" in tool_config:
255
+ info(f"Documentation: {tool_config['docs_url']}")
256
+
257
+ elif install_type == "script":
258
+ install_url = tool_config["install_url"]
259
+ expected_sha256 = tool_config.get("install_sha256")
260
+ if run_install_script(
261
+ install_url, tool_config["name"], expected_sha256, dryrun
262
+ ):
263
+ if dryrun:
264
+ success(f"{tool_config['name']} verification passed")
265
+ else:
266
+ success(f"{tool_config['name']} installed successfully")
267
+ info(tool_config["next_steps"])
268
+ if "docs_url" in tool_config:
269
+ info(f"Documentation: {tool_config['docs_url']}")
270
+ else:
271
+ return False
272
+
273
+ elif install_type == "direct_download":
274
+ if install_direct_download(tool_name, tool_config, dryrun):
275
+ if dryrun:
276
+ success(f"{tool_config['name']} verification passed")
277
+ else:
278
+ info(tool_config["next_steps"])
279
+ if "docs_url" in tool_config:
280
+ info(f"Documentation: {tool_config['docs_url']}")
281
+ else:
282
+ return False
283
+
284
+ elif install_type == "self_managed":
285
+ npm_package = tool_config.get("npm_package")
286
+ if not npm_package:
287
+ error(f"No npm package configured for {tool_config['name']}")
288
+ return False
289
+ if dryrun:
290
+ info(f"[DRYRUN] Would install npm package: {npm_package}")
291
+ else:
292
+ run_command(["npm", "install", "-g", npm_package], check=True)
293
+ success(f"{tool_config['name']} installed successfully")
294
+ info(tool_config["next_steps"])
295
+ if "docs_url" in tool_config:
296
+ info(f"Documentation: {tool_config['docs_url']}")
297
+
298
+ return True
299
+
300
+ except subprocess.CalledProcessError as exc:
301
+ error(f"Failed to install {tool_config['name']}: {exc.stderr}")
302
+ return False
303
+ except Exception as exc:
304
+ error(f"Failed to install {tool_config['name']}: {exc}")
305
+ return False
@@ -0,0 +1,264 @@
1
+ """Upgrade and remove operations for managed tools."""
2
+
3
+ import glob as globmod
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from typing import List
9
+
10
+ from code_aide.constants import TOOLS
11
+ from code_aide.detection import detect_install_method
12
+ from code_aide.install import install_direct_download, run_install_script
13
+ from code_aide.console import error, info, run_command, success, warning
14
+ from code_aide.prereqs import is_tool_installed
15
+
16
+
17
+ def upgrade_tool(tool_name: str) -> bool:
18
+ """Upgrade a tool based on its configuration."""
19
+ tool_config = TOOLS.get(tool_name)
20
+ if not tool_config:
21
+ error(f"Unknown tool: {tool_name}")
22
+ return False
23
+
24
+ if not is_tool_installed(tool_name):
25
+ warning(f"{tool_config['name']} is not installed. Use 'install' command first.")
26
+ return False
27
+
28
+ install_info = detect_install_method(tool_name)
29
+ method = install_info["method"]
30
+ detail = install_info["detail"]
31
+
32
+ info(f"Upgrading {tool_config['name']} (installed via {method})...")
33
+
34
+ try:
35
+ if method == "brew_formula":
36
+ run_command(["brew", "upgrade", detail], check=True, capture=False)
37
+ success(f"{tool_config['name']} upgraded successfully")
38
+
39
+ elif method == "brew_cask":
40
+ run_command(
41
+ ["brew", "upgrade", "--cask", detail], check=True, capture=False
42
+ )
43
+ success(f"{tool_config['name']} upgraded successfully")
44
+
45
+ elif method in ("npm", "brew_npm"):
46
+ npm_package = detail or tool_config.get("npm_package")
47
+ if not npm_package:
48
+ error(f"No npm package configured for {tool_config['name']}")
49
+ return False
50
+ run_command(["npm", "install", "-g", f"{npm_package}@latest"], check=True)
51
+ success(f"{tool_config['name']} upgraded successfully")
52
+
53
+ elif method == "script":
54
+ install_url = tool_config["install_url"]
55
+ expected_sha256 = tool_config.get("install_sha256")
56
+ if run_install_script(install_url, tool_config["name"], expected_sha256):
57
+ success(f"{tool_config['name']} upgraded successfully")
58
+ else:
59
+ return False
60
+
61
+ elif method == "direct_download":
62
+ if not install_direct_download(tool_name, tool_config):
63
+ return False
64
+
65
+ elif method == "self_managed":
66
+ upgrade_cmd = tool_config.get("upgrade_command")
67
+ if not upgrade_cmd:
68
+ error(f"No upgrade_command configured for {tool_config['name']}")
69
+ return False
70
+ run_command(upgrade_cmd, check=True, capture=False)
71
+ success(f"{tool_config['name']} upgraded successfully")
72
+
73
+ elif method == "system":
74
+ error(
75
+ f"{tool_config['name']} is managed by the system package manager. "
76
+ "Use your package manager to upgrade it."
77
+ )
78
+ return False
79
+
80
+ else:
81
+ error(
82
+ f"Don't know how to upgrade {tool_config['name']} "
83
+ f"(install method: {method})"
84
+ )
85
+ return False
86
+
87
+ return True
88
+
89
+ except subprocess.CalledProcessError as exc:
90
+ error(f"Failed to upgrade {tool_config['name']}: {exc.stderr}")
91
+ return False
92
+ except Exception as exc:
93
+ error(f"Failed to upgrade {tool_config['name']}: {exc}")
94
+ return False
95
+
96
+
97
+ def remove_tool(tool_name: str) -> bool:
98
+ """Remove a tool based on its configuration."""
99
+ tool_config = TOOLS.get(tool_name)
100
+ if not tool_config:
101
+ error(f"Unknown tool: {tool_name}")
102
+ return False
103
+
104
+ if not is_tool_installed(tool_name):
105
+ warning(f"{tool_config['name']} is not installed.")
106
+ return True
107
+
108
+ install_info = detect_install_method(tool_name)
109
+ method = install_info["method"]
110
+ detail = install_info["detail"]
111
+
112
+ info(f"Removing {tool_config['name']} (installed via {method})...")
113
+
114
+ try:
115
+ if method == "brew_formula":
116
+ run_command(["brew", "uninstall", detail], check=True, capture=False)
117
+ success(f"{tool_config['name']} removed successfully")
118
+
119
+ elif method == "brew_cask":
120
+ run_command(
121
+ ["brew", "uninstall", "--cask", detail], check=True, capture=False
122
+ )
123
+ success(f"{tool_config['name']} removed successfully")
124
+
125
+ elif method in ("npm", "brew_npm"):
126
+ npm_package = detail or tool_config.get("npm_package")
127
+ if not npm_package:
128
+ error(f"No npm package configured for {tool_config['name']}")
129
+ return False
130
+ run_command(["npm", "uninstall", "-g", npm_package], check=True)
131
+ success(f"{tool_config['name']} removed successfully")
132
+
133
+ elif method == "script":
134
+ command = tool_config["command"]
135
+ command_path = shutil.which(command)
136
+
137
+ if command_path:
138
+ try:
139
+ os.remove(command_path)
140
+ success(f"{tool_config['name']} removed successfully")
141
+ except PermissionError:
142
+ try:
143
+ run_command(
144
+ ["sudo", "rm", command_path], check=True, capture=False
145
+ )
146
+ success(f"{tool_config['name']} removed successfully")
147
+ except subprocess.CalledProcessError as exc:
148
+ error(
149
+ f"Failed to remove {tool_config['name']}: {exc.stderr}. "
150
+ f"Please remove manually: {command_path}"
151
+ )
152
+ return False
153
+ except Exception as exc:
154
+ error(f"Failed to remove {tool_config['name']}: {exc}")
155
+ return False
156
+ else:
157
+ warning(f"Could not find {command} binary to remove")
158
+ return True
159
+
160
+ elif method == "direct_download":
161
+ bin_dir = os.path.expanduser(tool_config.get("bin_dir", "~/.local/bin"))
162
+ removed_links = set()
163
+ for link_name in tool_config.get("symlinks", {}):
164
+ link_path = os.path.join(bin_dir, link_name)
165
+ if os.path.lexists(link_path):
166
+ os.remove(link_path)
167
+ info(f"Removed symlink: {link_path}")
168
+ removed_links.add(link_path)
169
+
170
+ command_path = shutil.which(tool_config["command"])
171
+ if (
172
+ command_path
173
+ and command_path not in removed_links
174
+ and os.path.lexists(command_path)
175
+ ):
176
+ os.remove(command_path)
177
+ info(f"Removed: {command_path}")
178
+
179
+ install_dir_template = tool_config.get("install_dir")
180
+ if install_dir_template:
181
+ if "{version}" in install_dir_template:
182
+ install_pattern = os.path.expanduser(
183
+ install_dir_template.replace("{version}", "*")
184
+ )
185
+ for install_path in sorted(globmod.glob(install_pattern)):
186
+ if os.path.isdir(install_path):
187
+ shutil.rmtree(install_path)
188
+ info(f"Removed: {install_path}")
189
+ elif os.path.lexists(install_path):
190
+ os.remove(install_path)
191
+ info(f"Removed: {install_path}")
192
+ else:
193
+ install_path = os.path.expanduser(install_dir_template)
194
+ if os.path.isdir(install_path):
195
+ shutil.rmtree(install_path)
196
+ info(f"Removed: {install_path}")
197
+ elif os.path.lexists(install_path):
198
+ os.remove(install_path)
199
+ info(f"Removed: {install_path}")
200
+
201
+ success(f"{tool_config['name']} removed successfully")
202
+
203
+ elif method == "self_managed":
204
+ command = tool_config["command"]
205
+ command_path = shutil.which(command)
206
+ removed_any = False
207
+ if command_path:
208
+ real_path = os.path.realpath(command_path)
209
+ remove_paths = [command_path]
210
+ if real_path != command_path:
211
+ remove_paths.append(real_path)
212
+
213
+ for path in remove_paths:
214
+ if not os.path.lexists(path):
215
+ continue
216
+ is_link = os.path.islink(path)
217
+ if os.path.isdir(path) and not is_link:
218
+ shutil.rmtree(path)
219
+ else:
220
+ os.remove(path)
221
+ if is_link:
222
+ info(f"Removed symlink: {path}")
223
+ else:
224
+ info(f"Removed: {path}")
225
+ removed_any = True
226
+
227
+ if removed_any:
228
+ success(f"{tool_config['name']} removed successfully")
229
+ else:
230
+ warning(f"Could not find {command} binary to remove")
231
+
232
+ elif method == "system":
233
+ error(
234
+ f"{tool_config['name']} is managed by the system package manager. "
235
+ "Use your package manager to remove it."
236
+ )
237
+ return False
238
+
239
+ else:
240
+ error(
241
+ f"Don't know how to remove {tool_config['name']} "
242
+ f"(install method: {method})"
243
+ )
244
+ return False
245
+
246
+ return True
247
+
248
+ except subprocess.CalledProcessError as exc:
249
+ error(f"Failed to remove {tool_config['name']}: {exc.stderr}")
250
+ return False
251
+ except Exception as exc:
252
+ error(f"Failed to remove {tool_config['name']}: {exc}")
253
+ return False
254
+
255
+
256
+ def validate_tools(tools: List[str]) -> None:
257
+ """Validate that all tool names are valid."""
258
+ invalid_tools = [tool for tool in tools if tool not in TOOLS]
259
+
260
+ if invalid_tools:
261
+ error(f"Invalid tool name(s): {', '.join(invalid_tools)}")
262
+ available = ", ".join(TOOLS.keys())
263
+ print(f"\nAvailable tools: {available}")
264
+ sys.exit(1)