ndev-stack 0.1.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.
- ndev/__init__.py +8 -0
- ndev/__main__.py +4 -0
- ndev/cli.py +24 -0
- ndev/common/__init__.py +3 -0
- ndev/common/config.py +114 -0
- ndev/common/constants.py +51 -0
- ndev/common/github.py +13 -0
- ndev/common/logger.py +11 -0
- ndev/common/manifest.py +41 -0
- ndev/common/utils.py +96 -0
- ndev/linux/__init__.py +1 -0
- ndev/linux/chroot/manager.py +63 -0
- ndev/linux/chroot/packages.py +91 -0
- ndev/linux/chroot/shell.py +9 -0
- ndev/linux/cli.py +235 -0
- ndev/linux/commands/available.py +38 -0
- ndev/linux/commands/clean.py +25 -0
- ndev/linux/commands/ctl.py +192 -0
- ndev/linux/commands/current.py +11 -0
- ndev/linux/commands/db.py +319 -0
- ndev/linux/commands/doctor.py +56 -0
- ndev/linux/commands/grok.py +75 -0
- ndev/linux/commands/install.py +39 -0
- ndev/linux/commands/list.py +47 -0
- ndev/linux/commands/logs.py +36 -0
- ndev/linux/commands/mailpit.py +82 -0
- ndev/linux/commands/reload.py +26 -0
- ndev/linux/commands/restart.py +34 -0
- ndev/linux/commands/setup.py +113 -0
- ndev/linux/commands/start.py +34 -0
- ndev/linux/commands/status.py +81 -0
- ndev/linux/commands/stop.py +34 -0
- ndev/linux/commands/uninstall.py +69 -0
- ndev/linux/commands/update.py +57 -0
- ndev/linux/commands/upgrade.py +81 -0
- ndev/linux/commands/use.py +108 -0
- ndev/linux/commands/vhost.py +350 -0
- ndev/linux/php/builder.py +183 -0
- ndev/linux/php/downloader.py +58 -0
- ndev/linux/php/extensions.py +146 -0
- ndev/linux/php/installer.py +42 -0
- ndev/linux/php/resolver.py +59 -0
- ndev/linux/php/templates.py +128 -0
- ndev/linux/runtime/fpm.py +117 -0
- ndev/linux/runtime/mailpit.py +244 -0
- ndev/linux/runtime/pma.py +223 -0
- ndev/linux/runtime/process.py +37 -0
- ndev/linux/runtime/sockets.py +16 -0
- ndev/linux/runtime/upgrade.py +431 -0
- ndev/linux/tui.py +1423 -0
- ndev/main.py +52 -0
- ndev/tui.py +23 -0
- ndev/win/__init__.py +1 -0
- ndev/win/cli.py +1898 -0
- ndev/win/commands/__init__.py +0 -0
- ndev/win/core/__init__.py +0 -0
- ndev/win/core/db.py +265 -0
- ndev/win/core/elevate.py +94 -0
- ndev/win/core/ext.py +241 -0
- ndev/win/core/fcgi.py +216 -0
- ndev/win/core/grok.py +55 -0
- ndev/win/core/logs.py +66 -0
- ndev/win/core/mailpit.py +236 -0
- ndev/win/core/mkcert.py +65 -0
- ndev/win/core/paths.py +85 -0
- ndev/win/core/php.py +533 -0
- ndev/win/core/pma.py +190 -0
- ndev/win/core/services.py +349 -0
- ndev/win/core/setup.py +361 -0
- ndev/win/core/upgrade.py +513 -0
- ndev/win/core/vhost.py +289 -0
- ndev/win/templates/vhost.conf.tmpl +33 -0
- ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
- ndev/win/tui.py +1313 -0
- ndev_stack-0.1.0.dist-info/METADATA +553 -0
- ndev_stack-0.1.0.dist-info/RECORD +79 -0
- ndev_stack-0.1.0.dist-info/WHEEL +5 -0
- ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
- ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component version check and upgrade management for Linux.
|
|
3
|
+
Manages updates for Nginx, Mailpit, MariaDB, phpMyAdmin, mkcert, and Composer on Linux.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import urllib.request
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from ndev.common.constants import NDEV_DIR, LOGS_DIR
|
|
17
|
+
from ndev.linux.runtime import mailpit as mailpit_rt, pma as pma_rt
|
|
18
|
+
|
|
19
|
+
USER_AGENT = "ndev/0.1.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ComponentInfo:
|
|
24
|
+
name: str
|
|
25
|
+
display_name: str
|
|
26
|
+
current_version: Optional[str]
|
|
27
|
+
latest_version: Optional[str]
|
|
28
|
+
update_available: bool
|
|
29
|
+
installed: bool
|
|
30
|
+
status: str
|
|
31
|
+
error: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _http_get_json(url: str, timeout: int = 10) -> dict:
|
|
35
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
36
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
37
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clean_ver(v: Optional[str]) -> str:
|
|
41
|
+
if not v:
|
|
42
|
+
return ""
|
|
43
|
+
v = v.strip().lower()
|
|
44
|
+
if v.startswith("v"):
|
|
45
|
+
v = v[1:]
|
|
46
|
+
return v.split()[0]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# 1. NGINX
|
|
50
|
+
def get_nginx_info() -> ComponentInfo:
|
|
51
|
+
installed = bool(shutil.which("nginx"))
|
|
52
|
+
curr_ver = None
|
|
53
|
+
latest_ver = None
|
|
54
|
+
err = None
|
|
55
|
+
|
|
56
|
+
if installed:
|
|
57
|
+
try:
|
|
58
|
+
res = subprocess.run(["nginx", "-v"], capture_output=True, text=True, timeout=5)
|
|
59
|
+
out = res.stderr or res.stdout
|
|
60
|
+
m = re.search(r"nginx/(\d+\.\d+\.\d+)", out)
|
|
61
|
+
if m:
|
|
62
|
+
curr_ver = m.group(1)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
err = str(e)
|
|
65
|
+
|
|
66
|
+
# Check apt update availability if on Debian/Ubuntu
|
|
67
|
+
update_avail = False
|
|
68
|
+
if installed and shutil.which("apt"):
|
|
69
|
+
try:
|
|
70
|
+
res = subprocess.run(["apt", "list", "--upgradable", "nginx"], capture_output=True, text=True, timeout=10)
|
|
71
|
+
if "nginx" in res.stdout and "upgradable from" in res.stdout:
|
|
72
|
+
update_avail = True
|
|
73
|
+
m = re.search(r"nginx/[\w\-]+\s+(\S+)", res.stdout)
|
|
74
|
+
if m:
|
|
75
|
+
latest_ver = m.group(1)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
if not latest_ver and curr_ver:
|
|
80
|
+
latest_ver = curr_ver
|
|
81
|
+
|
|
82
|
+
status = "Up-to-date"
|
|
83
|
+
if not installed:
|
|
84
|
+
status = "Not Installed"
|
|
85
|
+
elif update_avail:
|
|
86
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
87
|
+
|
|
88
|
+
return ComponentInfo("nginx", "Nginx Web Server", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def upgrade_nginx() -> tuple[bool, str]:
|
|
92
|
+
if not shutil.which("apt"):
|
|
93
|
+
return False, "Package manager 'apt' not found."
|
|
94
|
+
try:
|
|
95
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
|
96
|
+
subprocess.run(["sudo", "apt-get", "--only-upgrade", "install", "-y", "nginx"], check=True)
|
|
97
|
+
info = get_nginx_info()
|
|
98
|
+
return True, f"Nginx upgraded successfully ({info.current_version})."
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return False, f"Nginx upgrade failed: {e}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# 2. MAILPIT
|
|
104
|
+
def get_mailpit_info() -> ComponentInfo:
|
|
105
|
+
installed = mailpit_rt.is_installed()
|
|
106
|
+
curr_ver = None
|
|
107
|
+
latest_ver = None
|
|
108
|
+
err = None
|
|
109
|
+
|
|
110
|
+
if installed:
|
|
111
|
+
try:
|
|
112
|
+
res = subprocess.run([str(mailpit_rt.BINARY_PATH), "version"], capture_output=True, text=True, timeout=5)
|
|
113
|
+
out = res.stdout.strip() or res.stderr.strip()
|
|
114
|
+
m = re.search(r"v?(\d+\.\d+\.\d+)", out)
|
|
115
|
+
if m:
|
|
116
|
+
curr_ver = f"v{m.group(1)}"
|
|
117
|
+
else:
|
|
118
|
+
curr_ver = out
|
|
119
|
+
except Exception as e:
|
|
120
|
+
err = str(e)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
data = _http_get_json("https://api.github.com/repos/axllent/mailpit/releases/latest")
|
|
124
|
+
latest_ver = data.get("tag_name")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
if not err:
|
|
127
|
+
err = f"Could not query GitHub releases: {e}"
|
|
128
|
+
|
|
129
|
+
update_avail = False
|
|
130
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
131
|
+
update_avail = True
|
|
132
|
+
|
|
133
|
+
status = "Up-to-date"
|
|
134
|
+
if not installed:
|
|
135
|
+
status = "Not Installed"
|
|
136
|
+
elif update_avail:
|
|
137
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
138
|
+
|
|
139
|
+
return ComponentInfo("mailpit", "Mailpit Email Sandbox", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def upgrade_mailpit() -> tuple[bool, str]:
|
|
143
|
+
was_running = mailpit_rt.get_mailpit_status().get("running", False)
|
|
144
|
+
if was_running:
|
|
145
|
+
mailpit_rt.stop_mailpit()
|
|
146
|
+
try:
|
|
147
|
+
mailpit_rt.install_mailpit()
|
|
148
|
+
if was_running:
|
|
149
|
+
mailpit_rt.start_mailpit()
|
|
150
|
+
info = get_mailpit_info()
|
|
151
|
+
return True, f"Mailpit upgraded successfully to {info.current_version}."
|
|
152
|
+
except Exception as e:
|
|
153
|
+
if was_running:
|
|
154
|
+
try:
|
|
155
|
+
mailpit_rt.start_mailpit()
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
return False, f"Mailpit upgrade failed: {e}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# 3. MARIADB
|
|
162
|
+
def get_mariadb_info() -> ComponentInfo:
|
|
163
|
+
installed = bool(shutil.which("mariadb") or shutil.which("mysql") or shutil.which("mysqld"))
|
|
164
|
+
curr_ver = None
|
|
165
|
+
latest_ver = None
|
|
166
|
+
err = None
|
|
167
|
+
|
|
168
|
+
if installed:
|
|
169
|
+
try:
|
|
170
|
+
exe = shutil.which("mariadbd") or shutil.which("mysqld") or shutil.which("mysql")
|
|
171
|
+
if exe:
|
|
172
|
+
res = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=5)
|
|
173
|
+
m = re.search(r"(\d+\.\d+\.\d+)", res.stdout or res.stderr)
|
|
174
|
+
if m:
|
|
175
|
+
curr_ver = m.group(1)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
err = str(e)
|
|
178
|
+
|
|
179
|
+
update_avail = False
|
|
180
|
+
if installed and shutil.which("apt"):
|
|
181
|
+
try:
|
|
182
|
+
res = subprocess.run(["apt", "list", "--upgradable", "mariadb-server"], capture_output=True, text=True, timeout=10)
|
|
183
|
+
if "mariadb-server" in res.stdout and "upgradable from" in res.stdout:
|
|
184
|
+
update_avail = True
|
|
185
|
+
m = re.search(r"mariadb-server/[\w\-]+\s+(\S+)", res.stdout)
|
|
186
|
+
if m:
|
|
187
|
+
latest_ver = m.group(1)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
if not latest_ver and curr_ver:
|
|
192
|
+
latest_ver = curr_ver
|
|
193
|
+
|
|
194
|
+
status = "Up-to-date"
|
|
195
|
+
if not installed:
|
|
196
|
+
status = "Not Installed"
|
|
197
|
+
elif update_avail:
|
|
198
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
199
|
+
|
|
200
|
+
return ComponentInfo("mariadb", "MariaDB Server", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def upgrade_mariadb() -> tuple[bool, str]:
|
|
204
|
+
if not shutil.which("apt"):
|
|
205
|
+
return False, "Package manager 'apt' not found."
|
|
206
|
+
try:
|
|
207
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
|
208
|
+
subprocess.run(["sudo", "apt-get", "--only-upgrade", "install", "-y", "mariadb-server"], check=True)
|
|
209
|
+
info = get_mariadb_info()
|
|
210
|
+
return True, f"MariaDB upgraded successfully ({info.current_version})."
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return False, f"MariaDB upgrade failed: {e}"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# 4. PHPMYADMIN
|
|
216
|
+
def get_pma_info() -> ComponentInfo:
|
|
217
|
+
pma_st = pma_rt.get_pma_status()
|
|
218
|
+
installed = pma_st.get("installed", False)
|
|
219
|
+
curr_ver = None
|
|
220
|
+
pma_dir = pma_rt.PMA_DIR
|
|
221
|
+
if installed and pma_dir.exists():
|
|
222
|
+
for f in pma_dir.glob("RELEASE-DATE-*"):
|
|
223
|
+
curr_ver = f.name.replace("RELEASE-DATE-", "")
|
|
224
|
+
break
|
|
225
|
+
if not curr_ver:
|
|
226
|
+
curr_ver = pma_rt.DEFAULT_PMA_VERSION
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
data = _http_get_json("https://www.phpmyadmin.net/home_page/version.json")
|
|
230
|
+
latest_ver = data.get("version", pma_rt.DEFAULT_PMA_VERSION)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
latest_ver = pma_rt.DEFAULT_PMA_VERSION
|
|
233
|
+
if not err:
|
|
234
|
+
err = f"Could not query PMA version API: {e}"
|
|
235
|
+
|
|
236
|
+
update_avail = False
|
|
237
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
238
|
+
update_avail = True
|
|
239
|
+
|
|
240
|
+
status = "Up-to-date"
|
|
241
|
+
if not installed:
|
|
242
|
+
status = "Not Installed"
|
|
243
|
+
elif update_avail:
|
|
244
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
245
|
+
|
|
246
|
+
return ComponentInfo("pma", "PMA (phpMyAdmin)", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def upgrade_pma() -> tuple[bool, str]:
|
|
250
|
+
was_running = pma_rt.get_pma_status().get("running", False)
|
|
251
|
+
if was_running:
|
|
252
|
+
pma_rt.stop_pma()
|
|
253
|
+
|
|
254
|
+
info = get_pma_info()
|
|
255
|
+
target_ver = info.latest_version or pma_rt.DEFAULT_PMA_VERSION
|
|
256
|
+
|
|
257
|
+
# Preserve config.inc.php
|
|
258
|
+
pma_dir = pma_rt.PMA_DIR
|
|
259
|
+
config_inc = pma_dir / "config.inc.php"
|
|
260
|
+
config_backup = None
|
|
261
|
+
if config_inc.exists():
|
|
262
|
+
config_backup = NDEV_DIR / "cache" / "_pma_config.inc.php.bak"
|
|
263
|
+
config_backup.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
shutil.copy(config_inc, config_backup)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
pma_rt.install_pma(version=target_ver)
|
|
268
|
+
if config_backup and config_backup.exists():
|
|
269
|
+
shutil.copy(config_backup, pma_dir / "config.inc.php")
|
|
270
|
+
config_backup.unlink(missing_ok=True)
|
|
271
|
+
|
|
272
|
+
if was_running:
|
|
273
|
+
pma_rt.start_pma()
|
|
274
|
+
|
|
275
|
+
return True, f"phpMyAdmin upgraded successfully to {target_ver}."
|
|
276
|
+
except Exception as e:
|
|
277
|
+
if was_running:
|
|
278
|
+
try:
|
|
279
|
+
pma_rt.start_pma()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
return False, f"phpMyAdmin upgrade failed: {e}"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# 5. MKCERT
|
|
286
|
+
def get_mkcert_info() -> ComponentInfo:
|
|
287
|
+
installed = bool(shutil.which("mkcert"))
|
|
288
|
+
curr_ver = None
|
|
289
|
+
latest_ver = None
|
|
290
|
+
err = None
|
|
291
|
+
|
|
292
|
+
if installed:
|
|
293
|
+
try:
|
|
294
|
+
res = subprocess.run(["mkcert", "-version"], capture_output=True, text=True, timeout=5)
|
|
295
|
+
curr_ver = res.stdout.strip() or res.stderr.strip()
|
|
296
|
+
except Exception as e:
|
|
297
|
+
err = str(e)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
data = _http_get_json("https://api.github.com/repos/FiloSottile/mkcert/releases/latest")
|
|
301
|
+
latest_ver = data.get("tag_name")
|
|
302
|
+
except Exception as e:
|
|
303
|
+
latest_ver = "v1.4.4"
|
|
304
|
+
if not err:
|
|
305
|
+
err = f"Could not query GitHub releases: {e}"
|
|
306
|
+
|
|
307
|
+
update_avail = False
|
|
308
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
309
|
+
update_avail = True
|
|
310
|
+
|
|
311
|
+
status = "Up-to-date"
|
|
312
|
+
if not installed:
|
|
313
|
+
status = "Not Installed"
|
|
314
|
+
elif update_avail:
|
|
315
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
316
|
+
|
|
317
|
+
return ComponentInfo("mkcert", "mkcert Local SSL", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def upgrade_mkcert() -> tuple[bool, str]:
|
|
321
|
+
if not shutil.which("apt"):
|
|
322
|
+
return False, "Package manager 'apt' not found."
|
|
323
|
+
try:
|
|
324
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
|
325
|
+
subprocess.run(["sudo", "apt-get", "--only-upgrade", "install", "-y", "mkcert"], check=True)
|
|
326
|
+
subprocess.run(["mkcert", "-install"], check=True)
|
|
327
|
+
info = get_mkcert_info()
|
|
328
|
+
return True, f"mkcert upgraded successfully ({info.current_version})."
|
|
329
|
+
except Exception as e:
|
|
330
|
+
return False, f"mkcert upgrade failed: {e}"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# 6. COMPOSER
|
|
334
|
+
def get_composer_info() -> ComponentInfo:
|
|
335
|
+
installed = bool(shutil.which("composer"))
|
|
336
|
+
curr_ver = None
|
|
337
|
+
latest_ver = None
|
|
338
|
+
err = None
|
|
339
|
+
|
|
340
|
+
if installed:
|
|
341
|
+
try:
|
|
342
|
+
res = subprocess.run(["composer", "--version"], capture_output=True, text=True, timeout=10)
|
|
343
|
+
m = re.search(r"Composer (?:version )?(\d+\.\d+\.\d+)", res.stdout or res.stderr)
|
|
344
|
+
if m:
|
|
345
|
+
curr_ver = m.group(1)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
err = str(e)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
data = _http_get_json("https://getcomposer.org/versions")
|
|
351
|
+
stables = data.get("stable", [])
|
|
352
|
+
if stables:
|
|
353
|
+
latest_ver = stables[0].get("version")
|
|
354
|
+
except Exception as e:
|
|
355
|
+
if not err:
|
|
356
|
+
err = f"Could not query Composer versions API: {e}"
|
|
357
|
+
|
|
358
|
+
update_avail = False
|
|
359
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
360
|
+
update_avail = True
|
|
361
|
+
|
|
362
|
+
status = "Up-to-date"
|
|
363
|
+
if not installed:
|
|
364
|
+
status = "Not Installed"
|
|
365
|
+
elif update_avail:
|
|
366
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
367
|
+
|
|
368
|
+
return ComponentInfo("composer", "Composer PHP Package Manager", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def upgrade_composer() -> tuple[bool, str]:
|
|
372
|
+
try:
|
|
373
|
+
if shutil.which("composer"):
|
|
374
|
+
res = subprocess.run(["composer", "self-update"], capture_output=True, text=True, timeout=60)
|
|
375
|
+
if res.returncode == 0:
|
|
376
|
+
info = get_composer_info()
|
|
377
|
+
return True, f"Composer upgraded successfully ({info.current_version})."
|
|
378
|
+
|
|
379
|
+
# Fallback to direct download
|
|
380
|
+
dest = Path.home() / ".local" / "bin" / "composer"
|
|
381
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
phar_url = "https://getcomposer.org/composer-stable.phar"
|
|
383
|
+
req = urllib.request.Request(phar_url, headers={"User-Agent": USER_AGENT})
|
|
384
|
+
with urllib.request.urlopen(req, timeout=120) as resp, open(dest, "wb") as f:
|
|
385
|
+
f.write(resp.read())
|
|
386
|
+
dest.chmod(0o755)
|
|
387
|
+
info = get_composer_info()
|
|
388
|
+
return True, f"Composer upgraded successfully ({info.current_version})."
|
|
389
|
+
except Exception as e:
|
|
390
|
+
return False, f"Composer upgrade failed: {e}"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ── UNIFIED API ─────────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
COMPONENTS = ["nginx", "mailpit", "mariadb", "pma", "mkcert", "composer"]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def check_all() -> list[ComponentInfo]:
|
|
399
|
+
return [
|
|
400
|
+
get_nginx_info(),
|
|
401
|
+
get_mailpit_info(),
|
|
402
|
+
get_mariadb_info(),
|
|
403
|
+
get_pma_info(),
|
|
404
|
+
get_mkcert_info(),
|
|
405
|
+
get_composer_info(),
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def upgrade_component(name: str) -> tuple[bool, str]:
|
|
410
|
+
name = name.lower()
|
|
411
|
+
if name == "nginx":
|
|
412
|
+
return upgrade_nginx()
|
|
413
|
+
elif name in ["mailpit", "mail"]:
|
|
414
|
+
return upgrade_mailpit()
|
|
415
|
+
elif name in ["mariadb", "mysql"]:
|
|
416
|
+
return upgrade_mariadb()
|
|
417
|
+
elif name in ["pma", "phpmyadmin"]:
|
|
418
|
+
return upgrade_pma()
|
|
419
|
+
elif name == "mkcert":
|
|
420
|
+
return upgrade_mkcert()
|
|
421
|
+
elif name == "composer":
|
|
422
|
+
return upgrade_composer()
|
|
423
|
+
else:
|
|
424
|
+
return False, f"Unknown component '{name}'. Available: {', '.join(COMPONENTS)}"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def upgrade_all() -> dict[str, tuple[bool, str]]:
|
|
428
|
+
results = {}
|
|
429
|
+
for comp in COMPONENTS:
|
|
430
|
+
results[comp] = upgrade_component(comp)
|
|
431
|
+
return results
|