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/__init__.py +3 -0
- code_aide/__main__.py +6 -0
- code_aide/commands_actions.py +344 -0
- code_aide/commands_tools.py +183 -0
- code_aide/config.py +105 -0
- code_aide/console.py +58 -0
- code_aide/constants.py +63 -0
- code_aide/data/tools.json +100 -0
- code_aide/detection.py +184 -0
- code_aide/entry.py +112 -0
- code_aide/install.py +305 -0
- code_aide/operations.py +264 -0
- code_aide/prereqs.py +179 -0
- code_aide/status.py +86 -0
- code_aide/versions.py +356 -0
- code_aide-1.0.0.dist-info/METADATA +133 -0
- code_aide-1.0.0.dist-info/RECORD +20 -0
- code_aide-1.0.0.dist-info/WHEEL +4 -0
- code_aide-1.0.0.dist-info/entry_points.txt +2 -0
- code_aide-1.0.0.dist-info/licenses/LICENSE +191 -0
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
|
code_aide/operations.py
ADDED
|
@@ -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)
|