dev-setup 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.
- dev_setup/__init__.py +1 -0
- dev_setup/__main__.py +9 -0
- dev_setup/base.py +78 -0
- dev_setup/cli.py +38 -0
- dev_setup/commands/__init__.py +0 -0
- dev_setup/commands/add_cmd.py +185 -0
- dev_setup/commands/delete_cmd.py +44 -0
- dev_setup/commands/help_cmd.py +46 -0
- dev_setup/commands/install_cmd.py +96 -0
- dev_setup/commands/list_cmd.py +57 -0
- dev_setup/commands/remove_cmd.py +50 -0
- dev_setup/generic.py +302 -0
- dev_setup/packages/__init__.py +0 -0
- dev_setup/packages/aws_cli.py +73 -0
- dev_setup/packages/docker.py +93 -0
- dev_setup/packages/htop.py +53 -0
- dev_setup/packages/nvm.py +84 -0
- dev_setup/packages/php.py +65 -0
- dev_setup/packages/saml2aws.py +100 -0
- dev_setup/packages/starship.py +65 -0
- dev_setup/packages/uv_tool.py +68 -0
- dev_setup/registry.py +78 -0
- dev_setup/ui.py +97 -0
- dev_setup-1.0.0.dist-info/METADATA +358 -0
- dev_setup-1.0.0.dist-info/RECORD +27 -0
- dev_setup-1.0.0.dist-info/WHEEL +4 -0
- dev_setup-1.0.0.dist-info/entry_points.txt +2 -0
dev_setup/generic.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from dev_setup.base import Tool
|
|
10
|
+
|
|
11
|
+
CUSTOM_DIR = Path.home() / ".config" / "dev-setup" / "packages"
|
|
12
|
+
_CUSTOM_DIR = CUSTOM_DIR
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GenericTool(Tool):
|
|
16
|
+
category = "custom"
|
|
17
|
+
builtin = False
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
key: str,
|
|
22
|
+
name: str,
|
|
23
|
+
description: str = "",
|
|
24
|
+
category: str = "custom",
|
|
25
|
+
install_type: str = "unknown",
|
|
26
|
+
check_cmd: str = "",
|
|
27
|
+
npm_name: str = "",
|
|
28
|
+
pip_name: str = "",
|
|
29
|
+
git_url: str = "",
|
|
30
|
+
git_install_cmd: str = "",
|
|
31
|
+
git_remove_cmd: str = "",
|
|
32
|
+
apt_packages: str = "",
|
|
33
|
+
script_url: str = "",
|
|
34
|
+
install_script: str = "",
|
|
35
|
+
remove_script: str = "",
|
|
36
|
+
help_cmd: str = "",
|
|
37
|
+
) -> None:
|
|
38
|
+
self.key = key
|
|
39
|
+
self.name = name
|
|
40
|
+
self.description = description
|
|
41
|
+
self.category = category
|
|
42
|
+
self.install_type = install_type
|
|
43
|
+
self.check_cmd = check_cmd
|
|
44
|
+
self.npm_name = npm_name
|
|
45
|
+
self.pip_name = pip_name
|
|
46
|
+
self.git_url = git_url
|
|
47
|
+
self.git_install_cmd = git_install_cmd
|
|
48
|
+
self.git_remove_cmd = git_remove_cmd
|
|
49
|
+
self.apt_packages = apt_packages
|
|
50
|
+
self.script_url = script_url
|
|
51
|
+
self.install_script = install_script
|
|
52
|
+
self.remove_script = remove_script
|
|
53
|
+
self.help_cmd = help_cmd
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: dict, key: str) -> "GenericTool":
|
|
57
|
+
return cls(
|
|
58
|
+
key=key,
|
|
59
|
+
name=data.get("name", key),
|
|
60
|
+
description=data.get("description", ""),
|
|
61
|
+
category=data.get("category", "custom"),
|
|
62
|
+
install_type=data.get("type", "unknown"),
|
|
63
|
+
check_cmd=data.get("check_cmd", ""),
|
|
64
|
+
npm_name=data.get("npm_name", ""),
|
|
65
|
+
pip_name=data.get("pip_name", ""),
|
|
66
|
+
git_url=data.get("git_url", ""),
|
|
67
|
+
git_install_cmd=data.get("git_install_cmd", ""),
|
|
68
|
+
git_remove_cmd=data.get("git_remove_cmd", ""),
|
|
69
|
+
apt_packages=data.get("apt_packages", ""),
|
|
70
|
+
script_url=data.get("script_url", ""),
|
|
71
|
+
install_script=data.get("install_script", ""),
|
|
72
|
+
remove_script=data.get("remove_script", ""),
|
|
73
|
+
help_cmd=data.get("help_cmd", ""),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict:
|
|
77
|
+
d: dict = {
|
|
78
|
+
"name": self.name,
|
|
79
|
+
"description": self.description,
|
|
80
|
+
"category": self.category,
|
|
81
|
+
"type": self.install_type,
|
|
82
|
+
}
|
|
83
|
+
for field, val in [
|
|
84
|
+
("check_cmd", self.check_cmd),
|
|
85
|
+
("npm_name", self.npm_name),
|
|
86
|
+
("pip_name", self.pip_name),
|
|
87
|
+
("git_url", self.git_url),
|
|
88
|
+
("git_install_cmd", self.git_install_cmd),
|
|
89
|
+
("git_remove_cmd", self.git_remove_cmd),
|
|
90
|
+
("apt_packages", self.apt_packages),
|
|
91
|
+
("script_url", self.script_url),
|
|
92
|
+
("install_script", self.install_script),
|
|
93
|
+
("remove_script", self.remove_script),
|
|
94
|
+
("help_cmd", self.help_cmd),
|
|
95
|
+
]:
|
|
96
|
+
if val:
|
|
97
|
+
d[field] = val
|
|
98
|
+
return d
|
|
99
|
+
|
|
100
|
+
def save(self) -> None:
|
|
101
|
+
CUSTOM_DIR.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
path = CUSTOM_DIR / f"{self.key}.json"
|
|
103
|
+
path.write_text(json.dumps(self.to_dict(), indent=2))
|
|
104
|
+
|
|
105
|
+
def is_installed(self) -> bool:
|
|
106
|
+
if self.check_cmd:
|
|
107
|
+
return shutil.which(self.check_cmd) is not None
|
|
108
|
+
t = self.install_type
|
|
109
|
+
if t == "npm":
|
|
110
|
+
return self.npm_name and _npm_global_installed(self.npm_name)
|
|
111
|
+
if t == "pip":
|
|
112
|
+
return bool(self.pip_name) and shutil.which(self.pip_name) is not None
|
|
113
|
+
if t == "git":
|
|
114
|
+
return bool(self.git_url) and _git_clone_dest(self.git_url).exists()
|
|
115
|
+
if t == "apt":
|
|
116
|
+
return bool(self.apt_packages) and _apt_installed(self.apt_packages.split()[0])
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def get_version(self) -> str:
|
|
120
|
+
cmd = self.check_cmd or _type_cmd(self)
|
|
121
|
+
if not cmd or not shutil.which(cmd):
|
|
122
|
+
return ""
|
|
123
|
+
for flag in ["--version", "-v", "version"]:
|
|
124
|
+
try:
|
|
125
|
+
r = subprocess.run([cmd, flag], capture_output=True, text=True, timeout=5)
|
|
126
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
127
|
+
return r.stdout.strip().splitlines()[0]
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return "installed"
|
|
131
|
+
|
|
132
|
+
def install(self) -> Optional[str]:
|
|
133
|
+
from dev_setup import ui
|
|
134
|
+
t = self.install_type
|
|
135
|
+
|
|
136
|
+
if t == "npm":
|
|
137
|
+
if not self.npm_name:
|
|
138
|
+
raise RuntimeError("npm_name not set")
|
|
139
|
+
with ui.spinner(f"Installing {self.name} via npm..."):
|
|
140
|
+
subprocess.run(
|
|
141
|
+
["npm", "install", "-g", self.npm_name],
|
|
142
|
+
check=True, capture_output=True,
|
|
143
|
+
)
|
|
144
|
+
elif t == "pip":
|
|
145
|
+
if not self.pip_name:
|
|
146
|
+
raise RuntimeError("pip_name not set")
|
|
147
|
+
uv = shutil.which("uv")
|
|
148
|
+
with ui.spinner(f"Installing {self.name} via pip..."):
|
|
149
|
+
if uv:
|
|
150
|
+
subprocess.run(
|
|
151
|
+
[uv, "tool", "install", self.pip_name],
|
|
152
|
+
check=True, capture_output=True,
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
subprocess.run(
|
|
156
|
+
["pip3", "install", "--user", self.pip_name],
|
|
157
|
+
check=True, capture_output=True,
|
|
158
|
+
)
|
|
159
|
+
elif t == "git":
|
|
160
|
+
if not self.git_url:
|
|
161
|
+
raise RuntimeError("git_url not set")
|
|
162
|
+
dest = _git_clone_dest(self.git_url)
|
|
163
|
+
with ui.spinner(f"Cloning {self.name}..."):
|
|
164
|
+
subprocess.run(
|
|
165
|
+
["git", "clone", "--depth=1", self.git_url, str(dest)],
|
|
166
|
+
check=True, capture_output=True,
|
|
167
|
+
)
|
|
168
|
+
if self.git_install_cmd:
|
|
169
|
+
with ui.spinner(f"Running install command..."):
|
|
170
|
+
subprocess.run(
|
|
171
|
+
["bash", "-c", self.git_install_cmd],
|
|
172
|
+
cwd=dest, check=True, capture_output=True,
|
|
173
|
+
)
|
|
174
|
+
elif t == "apt":
|
|
175
|
+
if not self.apt_packages:
|
|
176
|
+
raise RuntimeError("apt_packages not set")
|
|
177
|
+
with ui.spinner(f"Installing {self.name} via apt..."):
|
|
178
|
+
subprocess.run(
|
|
179
|
+
["sudo", "apt-get", "install", "-y"] + self.apt_packages.split(),
|
|
180
|
+
check=True, capture_output=True,
|
|
181
|
+
)
|
|
182
|
+
elif t == "script":
|
|
183
|
+
if not self.script_url:
|
|
184
|
+
raise RuntimeError("script_url not set")
|
|
185
|
+
with ui.spinner(f"Running install script for {self.name}..."):
|
|
186
|
+
subprocess.run(
|
|
187
|
+
["bash", "-c", f"curl -fsSL '{self.script_url}' | sh"],
|
|
188
|
+
check=True, capture_output=True,
|
|
189
|
+
)
|
|
190
|
+
elif t == "bash":
|
|
191
|
+
if not self.install_script:
|
|
192
|
+
raise RuntimeError("install_script not set")
|
|
193
|
+
with ui.spinner(f"Installing {self.name}..."):
|
|
194
|
+
_run_bash_script(self.install_script)
|
|
195
|
+
else:
|
|
196
|
+
raise RuntimeError(f"Unsupported install type: {t!r}")
|
|
197
|
+
|
|
198
|
+
return self.get_version() or None
|
|
199
|
+
|
|
200
|
+
def remove(self) -> None:
|
|
201
|
+
from dev_setup import ui
|
|
202
|
+
t = self.install_type
|
|
203
|
+
|
|
204
|
+
if t == "npm":
|
|
205
|
+
with ui.spinner(f"Removing {self.name}..."):
|
|
206
|
+
subprocess.run(
|
|
207
|
+
["npm", "uninstall", "-g", self.npm_name],
|
|
208
|
+
check=True, capture_output=True,
|
|
209
|
+
)
|
|
210
|
+
elif t == "pip":
|
|
211
|
+
uv = shutil.which("uv")
|
|
212
|
+
with ui.spinner(f"Removing {self.name}..."):
|
|
213
|
+
if uv:
|
|
214
|
+
subprocess.run(
|
|
215
|
+
[uv, "tool", "uninstall", self.pip_name],
|
|
216
|
+
check=True, capture_output=True,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
subprocess.run(
|
|
220
|
+
["pip3", "uninstall", "-y", self.pip_name],
|
|
221
|
+
check=True, capture_output=True,
|
|
222
|
+
)
|
|
223
|
+
elif t == "git":
|
|
224
|
+
dest = _git_clone_dest(self.git_url)
|
|
225
|
+
if self.git_remove_cmd:
|
|
226
|
+
with ui.spinner(f"Running remove command..."):
|
|
227
|
+
subprocess.run(
|
|
228
|
+
["bash", "-c", self.git_remove_cmd],
|
|
229
|
+
cwd=dest, capture_output=True,
|
|
230
|
+
)
|
|
231
|
+
import shutil as _shutil
|
|
232
|
+
if dest.exists():
|
|
233
|
+
_shutil.rmtree(dest)
|
|
234
|
+
elif t == "apt":
|
|
235
|
+
with ui.spinner(f"Removing {self.name}..."):
|
|
236
|
+
subprocess.run(
|
|
237
|
+
["sudo", "apt-get", "remove", "-y"] + self.apt_packages.split(),
|
|
238
|
+
check=True, capture_output=True,
|
|
239
|
+
)
|
|
240
|
+
elif t == "script":
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
"Script-type packages cannot be auto-removed. "
|
|
243
|
+
"Remove manually then run: dev-setup delete " + self.key
|
|
244
|
+
)
|
|
245
|
+
elif t == "bash":
|
|
246
|
+
if not self.remove_script:
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
f"No remove script defined for '{self.key}'. "
|
|
249
|
+
"Remove manually then run: dev-setup delete " + self.key
|
|
250
|
+
)
|
|
251
|
+
with ui.spinner(f"Removing {self.name}..."):
|
|
252
|
+
_run_bash_script(self.remove_script)
|
|
253
|
+
else:
|
|
254
|
+
raise RuntimeError(f"Unsupported remove type: {t!r}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _npm_global_installed(pkg: str) -> bool:
|
|
258
|
+
try:
|
|
259
|
+
r = subprocess.run(["npm", "list", "-g", "--depth=0", pkg], capture_output=True, text=True)
|
|
260
|
+
return pkg in r.stdout
|
|
261
|
+
except Exception:
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _apt_installed(pkg: str) -> bool:
|
|
266
|
+
try:
|
|
267
|
+
r = subprocess.run(["dpkg", "-s", pkg], capture_output=True, text=True)
|
|
268
|
+
return "Status: install ok installed" in r.stdout
|
|
269
|
+
except Exception:
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _git_clone_dest(url: str) -> Path:
|
|
274
|
+
repo_name = url.rstrip("/").split("/")[-1].removesuffix(".git")
|
|
275
|
+
return Path.home() / ".local" / "share" / "dev-setup" / repo_name
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _run_bash_script(script: str) -> None:
|
|
279
|
+
"""Write script to a temp file and execute it with bash, capturing output."""
|
|
280
|
+
import os
|
|
281
|
+
import tempfile
|
|
282
|
+
|
|
283
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
|
|
284
|
+
f.write(script)
|
|
285
|
+
tmp = f.name
|
|
286
|
+
try:
|
|
287
|
+
result = subprocess.run(["bash", tmp], capture_output=True, text=True)
|
|
288
|
+
if result.returncode != 0:
|
|
289
|
+
raise RuntimeError(
|
|
290
|
+
result.stderr.strip() or f"Script exited with code {result.returncode}"
|
|
291
|
+
)
|
|
292
|
+
finally:
|
|
293
|
+
os.unlink(tmp)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _type_cmd(tool: GenericTool) -> str:
|
|
297
|
+
t = tool.install_type
|
|
298
|
+
if t == "npm":
|
|
299
|
+
return tool.npm_name
|
|
300
|
+
if t == "pip":
|
|
301
|
+
return tool.pip_name
|
|
302
|
+
return ""
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from dev_setup.base import Tool
|
|
12
|
+
|
|
13
|
+
_AWS_CLI_DIR = Path("/usr/local/aws-cli")
|
|
14
|
+
_AWS_BIN = Path("/usr/local/bin/aws")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AwsCliTool(Tool):
|
|
18
|
+
key = "aws"
|
|
19
|
+
name = "AWS CLI"
|
|
20
|
+
description = "Amazon Web Services command line interface (v2)"
|
|
21
|
+
category = "tools"
|
|
22
|
+
install_type = "script"
|
|
23
|
+
help_cmd = "aws help"
|
|
24
|
+
|
|
25
|
+
def is_installed(self) -> bool:
|
|
26
|
+
return shutil.which("aws") is not None
|
|
27
|
+
|
|
28
|
+
def get_version(self) -> str:
|
|
29
|
+
r = subprocess.run(["aws", "--version"], capture_output=True, text=True)
|
|
30
|
+
return r.stdout.strip() or r.stderr.strip()
|
|
31
|
+
|
|
32
|
+
def install(self) -> Optional[str]:
|
|
33
|
+
from dev_setup import ui
|
|
34
|
+
|
|
35
|
+
arch = "aarch64" if platform.machine() == "aarch64" else "x86_64"
|
|
36
|
+
url = f"https://awscli.amazonaws.com/awscli-exe-linux-{arch}.zip"
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
zip_path = Path(tmpdir) / "awscliv2.zip"
|
|
40
|
+
|
|
41
|
+
with ui.spinner("Downloading AWS CLI v2..."):
|
|
42
|
+
subprocess.run(
|
|
43
|
+
["curl", "-fsSL", url, "-o", str(zip_path)],
|
|
44
|
+
check=True, capture_output=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
with ui.spinner("Extracting AWS CLI..."):
|
|
48
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
49
|
+
zf.extractall(tmpdir)
|
|
50
|
+
|
|
51
|
+
with ui.spinner("Installing AWS CLI (sudo)..."):
|
|
52
|
+
subprocess.run(
|
|
53
|
+
["sudo", str(Path(tmpdir) / "aws" / "install")],
|
|
54
|
+
check=True, capture_output=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not self.is_installed():
|
|
58
|
+
raise RuntimeError("AWS CLI installation failed — aws binary not found")
|
|
59
|
+
return self.get_version()
|
|
60
|
+
|
|
61
|
+
def remove(self) -> None:
|
|
62
|
+
from dev_setup import ui
|
|
63
|
+
|
|
64
|
+
with ui.spinner("Removing AWS CLI..."):
|
|
65
|
+
for path in [
|
|
66
|
+
_AWS_CLI_DIR,
|
|
67
|
+
_AWS_BIN,
|
|
68
|
+
Path("/usr/local/bin/aws_completer"),
|
|
69
|
+
]:
|
|
70
|
+
if path.is_dir():
|
|
71
|
+
subprocess.run(["sudo", "rm", "-rf", str(path)], check=True, capture_output=True)
|
|
72
|
+
elif path.exists():
|
|
73
|
+
subprocess.run(["sudo", "rm", "-f", str(path)], check=True, capture_output=True)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from dev_setup.base import Tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DockerTool(Tool):
|
|
14
|
+
key = "docker"
|
|
15
|
+
name = "Docker"
|
|
16
|
+
description = "Container runtime + docker compose plugin"
|
|
17
|
+
category = "core"
|
|
18
|
+
install_type = "script"
|
|
19
|
+
help_cmd = "docker --help"
|
|
20
|
+
|
|
21
|
+
def is_installed(self) -> bool:
|
|
22
|
+
return shutil.which("docker") is not None
|
|
23
|
+
|
|
24
|
+
def get_version(self) -> str:
|
|
25
|
+
r = subprocess.run(["docker", "--version"], capture_output=True, text=True)
|
|
26
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
27
|
+
|
|
28
|
+
def install(self) -> Optional[str]:
|
|
29
|
+
from dev_setup import ui
|
|
30
|
+
|
|
31
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".sh", delete=False)
|
|
32
|
+
tmp.close()
|
|
33
|
+
try:
|
|
34
|
+
with ui.spinner("Downloading Docker install script..."):
|
|
35
|
+
subprocess.run(
|
|
36
|
+
["curl", "-fsSL", "https://get.docker.com", "-o", tmp.name],
|
|
37
|
+
check=True, capture_output=True,
|
|
38
|
+
)
|
|
39
|
+
with ui.spinner("Installing Docker (this may take a minute)..."):
|
|
40
|
+
subprocess.run(["sudo", "sh", tmp.name], check=True, capture_output=True)
|
|
41
|
+
finally:
|
|
42
|
+
Path(tmp.name).unlink(missing_ok=True)
|
|
43
|
+
|
|
44
|
+
user = os.environ.get("USER", "")
|
|
45
|
+
if user:
|
|
46
|
+
r = subprocess.run(["groups", user], capture_output=True, text=True)
|
|
47
|
+
if "docker" not in r.stdout:
|
|
48
|
+
ui.info(f"Adding {user} to docker group...")
|
|
49
|
+
subprocess.run(
|
|
50
|
+
["sudo", "usermod", "-aG", "docker", user],
|
|
51
|
+
check=True, capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
ui.warn("Log out and back in for docker group to take effect")
|
|
54
|
+
|
|
55
|
+
subprocess.run(["sudo", "systemctl", "enable", "docker"], capture_output=True)
|
|
56
|
+
subprocess.run(["sudo", "systemctl", "start", "docker"], capture_output=True)
|
|
57
|
+
self._ensure_compose(ui)
|
|
58
|
+
|
|
59
|
+
if not self.is_installed():
|
|
60
|
+
raise RuntimeError("Docker installation failed — docker binary not found")
|
|
61
|
+
return self.get_version()
|
|
62
|
+
|
|
63
|
+
def _ensure_compose(self, ui) -> None: # type: ignore[no-untyped-def]
|
|
64
|
+
r = subprocess.run(["docker", "compose", "version"], capture_output=True)
|
|
65
|
+
if r.returncode == 0:
|
|
66
|
+
return
|
|
67
|
+
ui.info("Installing docker-compose-plugin...")
|
|
68
|
+
for mgr_cmd in [
|
|
69
|
+
["sudo", "apt-get", "install", "-y", "docker-compose-plugin"],
|
|
70
|
+
["sudo", "yum", "install", "-y", "docker-compose-plugin"],
|
|
71
|
+
["sudo", "dnf", "install", "-y", "docker-compose-plugin"],
|
|
72
|
+
]:
|
|
73
|
+
r = subprocess.run(mgr_cmd, capture_output=True)
|
|
74
|
+
if r.returncode == 0:
|
|
75
|
+
return
|
|
76
|
+
ui.warn("docker-compose-plugin not available via package manager")
|
|
77
|
+
|
|
78
|
+
def remove(self) -> None:
|
|
79
|
+
from dev_setup import ui
|
|
80
|
+
|
|
81
|
+
with ui.spinner("Stopping Docker service..."):
|
|
82
|
+
subprocess.run(["sudo", "systemctl", "stop", "docker"], capture_output=True)
|
|
83
|
+
subprocess.run(["sudo", "systemctl", "disable", "docker"], capture_output=True)
|
|
84
|
+
|
|
85
|
+
with ui.spinner("Removing Docker packages..."):
|
|
86
|
+
subprocess.run(
|
|
87
|
+
["bash", "-c",
|
|
88
|
+
"sudo apt-get remove -y docker-ce docker-ce-cli containerd.io "
|
|
89
|
+
"docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras "
|
|
90
|
+
"2>/dev/null || sudo yum remove -y docker-ce docker-ce-cli containerd.io "
|
|
91
|
+
"docker-compose-plugin 2>/dev/null || true"],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from dev_setup.base import Tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HtopTool(Tool):
|
|
11
|
+
key = "htop"
|
|
12
|
+
name = "htop"
|
|
13
|
+
description = "Interactive process and resource monitor"
|
|
14
|
+
category = "tools"
|
|
15
|
+
install_type = "apt"
|
|
16
|
+
help_cmd = "man htop"
|
|
17
|
+
|
|
18
|
+
def is_installed(self) -> bool:
|
|
19
|
+
return shutil.which("htop") is not None
|
|
20
|
+
|
|
21
|
+
def get_version(self) -> str:
|
|
22
|
+
r = subprocess.run(["htop", "--version"], capture_output=True, text=True)
|
|
23
|
+
return r.stdout.strip().splitlines()[0] if r.returncode == 0 else ""
|
|
24
|
+
|
|
25
|
+
def install(self) -> Optional[str]:
|
|
26
|
+
from dev_setup import ui
|
|
27
|
+
|
|
28
|
+
with ui.spinner("Installing htop..."):
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["bash", "-c",
|
|
31
|
+
"sudo apt-get install -y htop 2>/dev/null || "
|
|
32
|
+
"sudo yum install -y htop 2>/dev/null || "
|
|
33
|
+
"sudo dnf install -y htop 2>/dev/null || "
|
|
34
|
+
"sudo pacman -S --noconfirm htop 2>/dev/null"],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not self.is_installed():
|
|
39
|
+
raise RuntimeError("htop installation failed — no supported package manager found")
|
|
40
|
+
return self.get_version()
|
|
41
|
+
|
|
42
|
+
def remove(self) -> None:
|
|
43
|
+
from dev_setup import ui
|
|
44
|
+
|
|
45
|
+
with ui.spinner("Removing htop..."):
|
|
46
|
+
subprocess.run(
|
|
47
|
+
["bash", "-c",
|
|
48
|
+
"sudo apt-get remove -y htop 2>/dev/null || "
|
|
49
|
+
"sudo yum remove -y htop 2>/dev/null || "
|
|
50
|
+
"sudo dnf remove -y htop 2>/dev/null || "
|
|
51
|
+
"sudo pacman -R --noconfirm htop 2>/dev/null"],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import urllib.request
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from dev_setup.base import Tool, patch_bashrc, remove_bashrc_block
|
|
10
|
+
|
|
11
|
+
NVM_DIR = Path.home() / ".nvm"
|
|
12
|
+
NVM_INIT_BLOCK = "nvm"
|
|
13
|
+
NVM_INIT_LINES = (
|
|
14
|
+
'export NVM_DIR="$HOME/.nvm"\n'
|
|
15
|
+
'[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n'
|
|
16
|
+
'[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NvmTool(Tool):
|
|
21
|
+
key = "nvm"
|
|
22
|
+
name = "NVM + Node LTS"
|
|
23
|
+
description = "Node Version Manager + latest Node LTS"
|
|
24
|
+
category = "core"
|
|
25
|
+
install_type = "script"
|
|
26
|
+
help_cmd = "nvm help"
|
|
27
|
+
|
|
28
|
+
def is_installed(self) -> bool:
|
|
29
|
+
return (NVM_DIR / "nvm.sh").exists()
|
|
30
|
+
|
|
31
|
+
def get_version(self) -> str:
|
|
32
|
+
r = subprocess.run(
|
|
33
|
+
["bash", "-c", f'. "{NVM_DIR}/nvm.sh" && nvm --version'],
|
|
34
|
+
capture_output=True, text=True,
|
|
35
|
+
)
|
|
36
|
+
return f"nvm v{r.stdout.strip()}" if r.returncode == 0 else ""
|
|
37
|
+
|
|
38
|
+
def install(self) -> Optional[str]:
|
|
39
|
+
from dev_setup import ui
|
|
40
|
+
|
|
41
|
+
tag = self._latest_release_tag()
|
|
42
|
+
url = f"https://raw.githubusercontent.com/nvm-sh/nvm/{tag}/install.sh"
|
|
43
|
+
|
|
44
|
+
with ui.spinner(f"Installing NVM {tag}..."):
|
|
45
|
+
subprocess.run(
|
|
46
|
+
["bash", "-c", f"curl -o- '{url}' | bash"],
|
|
47
|
+
check=True, capture_output=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not self.is_installed():
|
|
51
|
+
raise RuntimeError("NVM installation failed — nvm.sh not found")
|
|
52
|
+
|
|
53
|
+
patch_bashrc(NVM_INIT_BLOCK, NVM_INIT_LINES)
|
|
54
|
+
ui.success("NVM init added to ~/.bashrc")
|
|
55
|
+
|
|
56
|
+
with ui.spinner("Installing Node LTS..."):
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["bash", "-c", f'. "{NVM_DIR}/nvm.sh" && nvm install --lts'],
|
|
59
|
+
check=True, capture_output=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return self.get_version()
|
|
63
|
+
|
|
64
|
+
def remove(self) -> None:
|
|
65
|
+
from dev_setup import ui
|
|
66
|
+
|
|
67
|
+
if NVM_DIR.exists():
|
|
68
|
+
import shutil as _shutil
|
|
69
|
+
with ui.spinner("Removing NVM directory..."):
|
|
70
|
+
_shutil.rmtree(NVM_DIR)
|
|
71
|
+
|
|
72
|
+
remove_bashrc_block(NVM_INIT_BLOCK)
|
|
73
|
+
ui.info("NVM init removed from ~/.bashrc")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _latest_release_tag() -> str:
|
|
77
|
+
try:
|
|
78
|
+
url = "https://api.github.com/repos/nvm-sh/nvm/releases/latest"
|
|
79
|
+
req = urllib.request.Request(url, headers={"User-Agent": "dev-setup"})
|
|
80
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
81
|
+
data = json.loads(resp.read())
|
|
82
|
+
return data["tag_name"]
|
|
83
|
+
except Exception:
|
|
84
|
+
return "v0.40.3"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from dev_setup.base import Tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PhpTool(Tool):
|
|
11
|
+
key = "php"
|
|
12
|
+
name = "PHP 8.4"
|
|
13
|
+
description = "PHP 8.4 + common extensions via ondrej/php PPA"
|
|
14
|
+
category = "tools"
|
|
15
|
+
install_type = "apt"
|
|
16
|
+
help_cmd = "php --help"
|
|
17
|
+
|
|
18
|
+
def is_installed(self) -> bool:
|
|
19
|
+
return shutil.which("php") is not None
|
|
20
|
+
|
|
21
|
+
def get_version(self) -> str:
|
|
22
|
+
r = subprocess.run(["php", "--version"], capture_output=True, text=True)
|
|
23
|
+
return r.stdout.strip().splitlines()[0] if r.returncode == 0 else ""
|
|
24
|
+
|
|
25
|
+
def install(self) -> Optional[str]:
|
|
26
|
+
from dev_setup import ui
|
|
27
|
+
|
|
28
|
+
if not shutil.which("add-apt-repository"):
|
|
29
|
+
with ui.spinner("Installing software-properties-common..."):
|
|
30
|
+
subprocess.run(
|
|
31
|
+
["sudo", "apt-get", "install", "-y", "software-properties-common"],
|
|
32
|
+
check=True, capture_output=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
with ui.spinner("Adding ondrej/php PPA..."):
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["sudo", "add-apt-repository", "-y", "ppa:ondrej/php"],
|
|
38
|
+
check=True, capture_output=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
with ui.spinner("Updating package index..."):
|
|
42
|
+
subprocess.run(["sudo", "apt-get", "update", "-q"], check=True, capture_output=True)
|
|
43
|
+
|
|
44
|
+
packages = [
|
|
45
|
+
"php8.4", "php8.4-cli", "php8.4-common",
|
|
46
|
+
"php8.4-curl", "php8.4-mbstring", "php8.4-xml", "php8.4-zip",
|
|
47
|
+
]
|
|
48
|
+
with ui.spinner("Installing PHP 8.4 + extensions..."):
|
|
49
|
+
subprocess.run(
|
|
50
|
+
["sudo", "apt-get", "install", "-y"] + packages,
|
|
51
|
+
check=True, capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not self.is_installed():
|
|
55
|
+
raise RuntimeError("PHP installation failed")
|
|
56
|
+
return self.get_version()
|
|
57
|
+
|
|
58
|
+
def remove(self) -> None:
|
|
59
|
+
from dev_setup import ui
|
|
60
|
+
|
|
61
|
+
with ui.spinner("Removing PHP 8.4 packages..."):
|
|
62
|
+
subprocess.run(
|
|
63
|
+
["bash", "-c", "sudo apt-get remove -y 'php8.4*' && sudo apt-get autoremove -y"],
|
|
64
|
+
check=True, capture_output=True,
|
|
65
|
+
)
|