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
ndev/win/core/upgrade.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component version check and upgrade management for Windows.
|
|
3
|
+
Manages updates for Nginx, Mailpit, MariaDB, phpMyAdmin, mkcert, and Composer.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from . import fcgi, logs, mailpit as mailpit_core, paths, pma as pma_core, services, setup as setup_core
|
|
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 _http_get_text(url: str, timeout: int = 10) -> str:
|
|
41
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
42
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
43
|
+
return resp.read().decode("utf-8", errors="ignore")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _clean_ver(v: Optional[str]) -> str:
|
|
47
|
+
if not v:
|
|
48
|
+
return ""
|
|
49
|
+
v = v.strip().lower()
|
|
50
|
+
if v.startswith("v"):
|
|
51
|
+
v = v[1:]
|
|
52
|
+
# Remove git commit hashes or dates, e.g. "2.10.3 2026-..."
|
|
53
|
+
return v.split()[0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── COMPONENT DETECTORS & UPDATERS ──────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
# 1. NGINX
|
|
59
|
+
def get_nginx_info() -> ComponentInfo:
|
|
60
|
+
installed = services.nginx_is_installed()
|
|
61
|
+
curr_ver = None
|
|
62
|
+
latest_ver = None
|
|
63
|
+
err = None
|
|
64
|
+
|
|
65
|
+
if installed:
|
|
66
|
+
try:
|
|
67
|
+
res = subprocess.run([str(services.nginx_exe()), "-v"], capture_output=True, text=True, timeout=5)
|
|
68
|
+
out = res.stderr or res.stdout
|
|
69
|
+
m = re.search(r"nginx/(\d+\.\d+\.\d+)", out)
|
|
70
|
+
if m:
|
|
71
|
+
curr_ver = m.group(1)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
err = str(e)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
html = _http_get_text("https://nginx.org/en/download.html")
|
|
77
|
+
matches = re.findall(r"nginx-(\d+\.\d+\.\d+)\.zip", html)
|
|
78
|
+
if matches:
|
|
79
|
+
latest_ver = matches[0]
|
|
80
|
+
else:
|
|
81
|
+
latest_ver = setup_core.DEFAULT_NGINX_VERSION
|
|
82
|
+
except Exception as e:
|
|
83
|
+
latest_ver = setup_core.DEFAULT_NGINX_VERSION
|
|
84
|
+
if not err:
|
|
85
|
+
err = f"Could not query remote version: {e}"
|
|
86
|
+
|
|
87
|
+
update_avail = False
|
|
88
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
89
|
+
update_avail = True
|
|
90
|
+
|
|
91
|
+
status = "Up-to-date"
|
|
92
|
+
if not installed:
|
|
93
|
+
status = "Not Installed"
|
|
94
|
+
elif update_avail:
|
|
95
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
96
|
+
|
|
97
|
+
return ComponentInfo("nginx", "Nginx Web Server", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def upgrade_nginx() -> tuple[bool, str]:
|
|
101
|
+
was_running = services.nginx_is_running()
|
|
102
|
+
if was_running:
|
|
103
|
+
services.nginx_stop()
|
|
104
|
+
|
|
105
|
+
info = get_nginx_info()
|
|
106
|
+
target_ver = info.latest_version or setup_core.DEFAULT_NGINX_VERSION
|
|
107
|
+
|
|
108
|
+
# 1. Create a persistent safety backup of conf/
|
|
109
|
+
conf_dir = paths.NGINX_DIR / "conf"
|
|
110
|
+
if conf_dir.exists() and any(conf_dir.iterdir()):
|
|
111
|
+
import datetime
|
|
112
|
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
113
|
+
backup_dest = paths.BACKUPS_DIR / f"nginx_conf_{ts}"
|
|
114
|
+
try:
|
|
115
|
+
shutil.copytree(conf_dir, backup_dest)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
setup_core.install_nginx(version=target_ver)
|
|
121
|
+
|
|
122
|
+
if was_running:
|
|
123
|
+
services.nginx_start()
|
|
124
|
+
|
|
125
|
+
return True, f"Nginx upgraded successfully to {target_ver} (configuration preserved)."
|
|
126
|
+
except Exception as e:
|
|
127
|
+
if was_running:
|
|
128
|
+
try:
|
|
129
|
+
services.nginx_start()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
return False, f"Nginx upgrade failed: {e}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# 2. MAILPIT
|
|
136
|
+
def get_mailpit_info() -> ComponentInfo:
|
|
137
|
+
installed = mailpit_core.is_installed()
|
|
138
|
+
curr_ver = None
|
|
139
|
+
latest_ver = None
|
|
140
|
+
err = None
|
|
141
|
+
|
|
142
|
+
if installed:
|
|
143
|
+
try:
|
|
144
|
+
res = subprocess.run([str(mailpit_core.BINARY_PATH), "version"], capture_output=True, text=True, timeout=5)
|
|
145
|
+
out = res.stdout.strip() or res.stderr.strip()
|
|
146
|
+
m = re.search(r"v?(\d+\.\d+\.\d+)", out)
|
|
147
|
+
if m:
|
|
148
|
+
curr_ver = f"v{m.group(1)}"
|
|
149
|
+
else:
|
|
150
|
+
curr_ver = out
|
|
151
|
+
except Exception as e:
|
|
152
|
+
err = str(e)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
rel = mailpit_core._fetch_latest_release()
|
|
156
|
+
latest_ver = rel.get("tag_name")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
if not err:
|
|
159
|
+
err = f"Could not query GitHub releases: {e}"
|
|
160
|
+
|
|
161
|
+
update_avail = False
|
|
162
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
163
|
+
update_avail = True
|
|
164
|
+
|
|
165
|
+
status = "Up-to-date"
|
|
166
|
+
if not installed:
|
|
167
|
+
status = "Not Installed"
|
|
168
|
+
elif update_avail:
|
|
169
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
170
|
+
|
|
171
|
+
return ComponentInfo("mailpit", "Mailpit Email Sandbox", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def upgrade_mailpit() -> tuple[bool, str]:
|
|
175
|
+
was_running = bool(mailpit_core.status())
|
|
176
|
+
if was_running:
|
|
177
|
+
mailpit_core.stop()
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
paths.ensure_dirs()
|
|
181
|
+
release = mailpit_core._fetch_latest_release()
|
|
182
|
+
version = release.get("tag_name", "unknown")
|
|
183
|
+
asset = mailpit_core._find_windows_asset(release)
|
|
184
|
+
if not asset:
|
|
185
|
+
raise RuntimeError("No Windows asset found in latest Mailpit release.")
|
|
186
|
+
|
|
187
|
+
url = asset["browser_download_url"]
|
|
188
|
+
dl_path = paths.DOWNLOADS_DIR / asset["name"]
|
|
189
|
+
if dl_path.exists():
|
|
190
|
+
dl_path.unlink()
|
|
191
|
+
|
|
192
|
+
setup_core._download(url, dl_path)
|
|
193
|
+
with zipfile.ZipFile(dl_path) as zf:
|
|
194
|
+
for member in zf.namelist():
|
|
195
|
+
if member.lower().endswith(".exe") and "mailpit" in member.lower():
|
|
196
|
+
with zf.open(member) as src, open(mailpit_core.BINARY_PATH, "wb") as dst:
|
|
197
|
+
shutil.copyfileobj(src, dst)
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if was_running:
|
|
201
|
+
mailpit_core.start()
|
|
202
|
+
|
|
203
|
+
return True, f"Mailpit upgraded successfully to {version}."
|
|
204
|
+
except Exception as e:
|
|
205
|
+
if was_running:
|
|
206
|
+
try:
|
|
207
|
+
mailpit_core.start()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
return False, f"Mailpit upgrade failed: {e}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# 3. MARIADB
|
|
214
|
+
def get_mariadb_info() -> ComponentInfo:
|
|
215
|
+
installed = services.mariadb_is_installed()
|
|
216
|
+
curr_ver = None
|
|
217
|
+
latest_ver = None
|
|
218
|
+
err = None
|
|
219
|
+
|
|
220
|
+
if installed:
|
|
221
|
+
try:
|
|
222
|
+
mysqld = paths.MARIADB_DIR / "bin" / "mariadbd.exe"
|
|
223
|
+
if not mysqld.exists():
|
|
224
|
+
mysqld = paths.MARIADB_DIR / "bin" / "mysqld.exe"
|
|
225
|
+
if mysqld.exists():
|
|
226
|
+
res = subprocess.run([str(mysqld), "--version"], capture_output=True, text=True, timeout=5)
|
|
227
|
+
m = re.search(r"(\d+\.\d+\.\d+)", res.stdout or res.stderr)
|
|
228
|
+
if m:
|
|
229
|
+
curr_ver = m.group(1)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
err = str(e)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
data = _http_get_json("https://downloads.mariadb.org/rest-api/mariadb/11.4/")
|
|
235
|
+
releases = list(data.get("releases", {}).keys())
|
|
236
|
+
if releases:
|
|
237
|
+
latest_ver = releases[0]
|
|
238
|
+
else:
|
|
239
|
+
latest_ver = setup_core.DEFAULT_MARIADB_VERSION
|
|
240
|
+
except Exception as e:
|
|
241
|
+
latest_ver = setup_core.DEFAULT_MARIADB_VERSION
|
|
242
|
+
if not err:
|
|
243
|
+
err = f"Could not query MariaDB release API: {e}"
|
|
244
|
+
|
|
245
|
+
update_avail = False
|
|
246
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
247
|
+
update_avail = True
|
|
248
|
+
|
|
249
|
+
status = "Up-to-date"
|
|
250
|
+
if not installed:
|
|
251
|
+
status = "Not Installed"
|
|
252
|
+
elif update_avail:
|
|
253
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
254
|
+
|
|
255
|
+
return ComponentInfo("mariadb", "MariaDB Server", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def upgrade_mariadb() -> tuple[bool, str]:
|
|
259
|
+
mb_st = services.mariadb_status()
|
|
260
|
+
was_running = mb_st and mb_st.get("running")
|
|
261
|
+
if was_running:
|
|
262
|
+
services.mariadb_stop()
|
|
263
|
+
|
|
264
|
+
info = get_mariadb_info()
|
|
265
|
+
target_ver = info.latest_version or setup_core.DEFAULT_MARIADB_VERSION
|
|
266
|
+
|
|
267
|
+
# 1. Create a persistent safety snapshot backup of data/ and my.ini
|
|
268
|
+
data_dir = paths.MARIADB_DIR / "data"
|
|
269
|
+
my_ini = paths.MARIADB_DIR / "my.ini"
|
|
270
|
+
if data_dir.exists() and any(data_dir.iterdir()):
|
|
271
|
+
import datetime
|
|
272
|
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
273
|
+
backup_dest = paths.BACKUPS_DIR / f"mariadb_backup_{ts}"
|
|
274
|
+
try:
|
|
275
|
+
backup_dest.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
shutil.copytree(data_dir, backup_dest / "data")
|
|
277
|
+
if my_ini.exists():
|
|
278
|
+
shutil.copy2(my_ini, backup_dest / "my.ini")
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
setup_core.install_mariadb(version=target_ver)
|
|
284
|
+
|
|
285
|
+
if was_running:
|
|
286
|
+
services.mariadb_start()
|
|
287
|
+
|
|
288
|
+
return True, f"MariaDB upgraded successfully to {target_ver} (database and configuration preserved)."
|
|
289
|
+
except Exception as e:
|
|
290
|
+
if was_running:
|
|
291
|
+
try:
|
|
292
|
+
services.mariadb_start()
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
return False, f"MariaDB upgrade failed: {e}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# 4. PHPMYADMIN
|
|
299
|
+
def get_pma_info() -> ComponentInfo:
|
|
300
|
+
installed = (paths.PMA_DIR / "index.php").exists()
|
|
301
|
+
curr_ver = None
|
|
302
|
+
latest_ver = None
|
|
303
|
+
err = None
|
|
304
|
+
|
|
305
|
+
if installed:
|
|
306
|
+
for f in paths.PMA_DIR.glob("RELEASE-DATE-*"):
|
|
307
|
+
curr_ver = f.name.replace("RELEASE-DATE-", "")
|
|
308
|
+
break
|
|
309
|
+
if not curr_ver:
|
|
310
|
+
curr_ver = pma_core.DEFAULT_VERSION
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
data = _http_get_json("https://www.phpmyadmin.net/home_page/version.json")
|
|
314
|
+
latest_ver = data.get("version", pma_core.DEFAULT_VERSION)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
latest_ver = pma_core.DEFAULT_VERSION
|
|
317
|
+
if not err:
|
|
318
|
+
err = f"Could not query PMA version API: {e}"
|
|
319
|
+
|
|
320
|
+
update_avail = False
|
|
321
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
322
|
+
update_avail = True
|
|
323
|
+
|
|
324
|
+
status = "Up-to-date"
|
|
325
|
+
if not installed:
|
|
326
|
+
status = "Not Installed"
|
|
327
|
+
elif update_avail:
|
|
328
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
329
|
+
|
|
330
|
+
return ComponentInfo("pma", "PMA (phpMyAdmin)", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def upgrade_pma() -> tuple[bool, str]:
|
|
334
|
+
was_running = bool(pma_core.status())
|
|
335
|
+
if was_running:
|
|
336
|
+
pma_core.stop()
|
|
337
|
+
|
|
338
|
+
info = get_pma_info()
|
|
339
|
+
target_ver = info.latest_version or pma_core.DEFAULT_VERSION
|
|
340
|
+
|
|
341
|
+
# 1. Create safety backup of config.inc.php
|
|
342
|
+
config_inc = paths.PMA_DIR / "config.inc.php"
|
|
343
|
+
config_backup = None
|
|
344
|
+
if config_inc.exists():
|
|
345
|
+
import datetime
|
|
346
|
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
347
|
+
config_backup = paths.BACKUPS_DIR / f"pma_config_{ts}.inc.php"
|
|
348
|
+
try:
|
|
349
|
+
shutil.copy2(config_inc, config_backup)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
pma_core.install(version=target_ver)
|
|
355
|
+
if config_backup and config_backup.exists():
|
|
356
|
+
shutil.copy2(config_backup, paths.PMA_DIR / "config.inc.php")
|
|
357
|
+
|
|
358
|
+
if was_running:
|
|
359
|
+
pma_core.start()
|
|
360
|
+
|
|
361
|
+
return True, f"PMA upgraded successfully to {target_ver} (configuration preserved)."
|
|
362
|
+
except Exception as e:
|
|
363
|
+
if was_running:
|
|
364
|
+
try:
|
|
365
|
+
pma_core.start()
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return False, f"PMA upgrade failed: {e}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# 5. MKCERT
|
|
372
|
+
def get_mkcert_info() -> ComponentInfo:
|
|
373
|
+
mkcert_exe = paths.SHIM_DIR / "mkcert.exe"
|
|
374
|
+
installed = mkcert_exe.exists()
|
|
375
|
+
curr_ver = None
|
|
376
|
+
latest_ver = None
|
|
377
|
+
err = None
|
|
378
|
+
|
|
379
|
+
if installed:
|
|
380
|
+
try:
|
|
381
|
+
res = subprocess.run([str(mkcert_exe), "-version"], capture_output=True, text=True, timeout=5)
|
|
382
|
+
curr_ver = res.stdout.strip() or res.stderr.strip()
|
|
383
|
+
except Exception as e:
|
|
384
|
+
err = str(e)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
rel = _http_get_json("https://api.github.com/repos/FiloSottile/mkcert/releases/latest")
|
|
388
|
+
latest_ver = rel.get("tag_name")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
latest_ver = f"v{setup_core.DEFAULT_MKCERT_VERSION}"
|
|
391
|
+
if not err:
|
|
392
|
+
err = f"Could not query GitHub releases: {e}"
|
|
393
|
+
|
|
394
|
+
update_avail = False
|
|
395
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
396
|
+
update_avail = True
|
|
397
|
+
|
|
398
|
+
status = "Up-to-date"
|
|
399
|
+
if not installed:
|
|
400
|
+
status = "Not Installed"
|
|
401
|
+
elif update_avail:
|
|
402
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
403
|
+
|
|
404
|
+
return ComponentInfo("mkcert", "mkcert Local SSL", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def upgrade_mkcert() -> tuple[bool, str]:
|
|
408
|
+
info = get_mkcert_info()
|
|
409
|
+
target_ver = _clean_ver(info.latest_version) or setup_core.DEFAULT_MKCERT_VERSION
|
|
410
|
+
try:
|
|
411
|
+
dest = setup_core.install_mkcert(version=target_ver)
|
|
412
|
+
return True, f"mkcert upgraded successfully to v{target_ver}."
|
|
413
|
+
except Exception as e:
|
|
414
|
+
return False, f"mkcert upgrade failed: {e}"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# 6. COMPOSER
|
|
418
|
+
def get_composer_info() -> ComponentInfo:
|
|
419
|
+
phar = paths.SHIM_DIR / "composer.phar"
|
|
420
|
+
installed = phar.exists()
|
|
421
|
+
curr_ver = None
|
|
422
|
+
latest_ver = None
|
|
423
|
+
err = None
|
|
424
|
+
|
|
425
|
+
if installed:
|
|
426
|
+
try:
|
|
427
|
+
bat = paths.SHIM_DIR / "composer.bat"
|
|
428
|
+
res = subprocess.run([str(bat), "--version"], capture_output=True, text=True, timeout=10)
|
|
429
|
+
m = re.search(r"Composer (?:version )?(\d+\.\d+\.\d+)", res.stdout or res.stderr)
|
|
430
|
+
if m:
|
|
431
|
+
curr_ver = m.group(1)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
err = str(e)
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
data = _http_get_json("https://getcomposer.org/versions")
|
|
437
|
+
stables = data.get("stable", [])
|
|
438
|
+
if stables:
|
|
439
|
+
latest_ver = stables[0].get("version")
|
|
440
|
+
except Exception as e:
|
|
441
|
+
if not err:
|
|
442
|
+
err = f"Could not query Composer versions API: {e}"
|
|
443
|
+
|
|
444
|
+
update_avail = False
|
|
445
|
+
if curr_ver and latest_ver and _clean_ver(curr_ver) != _clean_ver(latest_ver):
|
|
446
|
+
update_avail = True
|
|
447
|
+
|
|
448
|
+
status = "Up-to-date"
|
|
449
|
+
if not installed:
|
|
450
|
+
status = "Not Installed"
|
|
451
|
+
elif update_avail:
|
|
452
|
+
status = f"Update Available ({curr_ver} -> {latest_ver})"
|
|
453
|
+
|
|
454
|
+
return ComponentInfo("composer", "Composer PHP Package Manager", curr_ver, latest_ver, update_avail, installed, status, err)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def upgrade_composer() -> tuple[bool, str]:
|
|
458
|
+
try:
|
|
459
|
+
bat = paths.SHIM_DIR / "composer.bat"
|
|
460
|
+
if bat.exists():
|
|
461
|
+
res = subprocess.run([str(bat), "self-update"], capture_output=True, text=True, timeout=60)
|
|
462
|
+
if res.returncode == 0:
|
|
463
|
+
info = get_composer_info()
|
|
464
|
+
return True, f"Composer upgraded successfully ({info.current_version})."
|
|
465
|
+
|
|
466
|
+
setup_core.install_composer()
|
|
467
|
+
info = get_composer_info()
|
|
468
|
+
return True, f"Composer upgraded successfully ({info.current_version or info.latest_version})."
|
|
469
|
+
except Exception as e:
|
|
470
|
+
return False, f"Composer upgrade failed: {e}"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# ── UNIFIED API ─────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
COMPONENTS = ["nginx", "mailpit", "mariadb", "pma", "mkcert", "composer"]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def check_all() -> list[ComponentInfo]:
|
|
479
|
+
"""Inspect installed versions and query latest upstream releases for all stack components."""
|
|
480
|
+
results = [
|
|
481
|
+
get_nginx_info(),
|
|
482
|
+
get_mailpit_info(),
|
|
483
|
+
get_mariadb_info(),
|
|
484
|
+
get_pma_info(),
|
|
485
|
+
get_mkcert_info(),
|
|
486
|
+
get_composer_info(),
|
|
487
|
+
]
|
|
488
|
+
return results
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def upgrade_component(name: str) -> tuple[bool, str]:
|
|
492
|
+
name = name.lower()
|
|
493
|
+
if name == "nginx":
|
|
494
|
+
return upgrade_nginx()
|
|
495
|
+
elif name in ["mailpit", "mail"]:
|
|
496
|
+
return upgrade_mailpit()
|
|
497
|
+
elif name in ["mariadb", "mysql"]:
|
|
498
|
+
return upgrade_mariadb()
|
|
499
|
+
elif name in ["pma", "phpmyadmin"]:
|
|
500
|
+
return upgrade_pma()
|
|
501
|
+
elif name == "mkcert":
|
|
502
|
+
return upgrade_mkcert()
|
|
503
|
+
elif name == "composer":
|
|
504
|
+
return upgrade_composer()
|
|
505
|
+
else:
|
|
506
|
+
return False, f"Unknown component '{name}'. Available: {', '.join(COMPONENTS)}"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def upgrade_all() -> dict[str, tuple[bool, str]]:
|
|
510
|
+
results = {}
|
|
511
|
+
for comp in COMPONENTS:
|
|
512
|
+
results[comp] = upgrade_component(comp)
|
|
513
|
+
return results
|