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.
Files changed (79) hide show
  1. ndev/__init__.py +8 -0
  2. ndev/__main__.py +4 -0
  3. ndev/cli.py +24 -0
  4. ndev/common/__init__.py +3 -0
  5. ndev/common/config.py +114 -0
  6. ndev/common/constants.py +51 -0
  7. ndev/common/github.py +13 -0
  8. ndev/common/logger.py +11 -0
  9. ndev/common/manifest.py +41 -0
  10. ndev/common/utils.py +96 -0
  11. ndev/linux/__init__.py +1 -0
  12. ndev/linux/chroot/manager.py +63 -0
  13. ndev/linux/chroot/packages.py +91 -0
  14. ndev/linux/chroot/shell.py +9 -0
  15. ndev/linux/cli.py +235 -0
  16. ndev/linux/commands/available.py +38 -0
  17. ndev/linux/commands/clean.py +25 -0
  18. ndev/linux/commands/ctl.py +192 -0
  19. ndev/linux/commands/current.py +11 -0
  20. ndev/linux/commands/db.py +319 -0
  21. ndev/linux/commands/doctor.py +56 -0
  22. ndev/linux/commands/grok.py +75 -0
  23. ndev/linux/commands/install.py +39 -0
  24. ndev/linux/commands/list.py +47 -0
  25. ndev/linux/commands/logs.py +36 -0
  26. ndev/linux/commands/mailpit.py +82 -0
  27. ndev/linux/commands/reload.py +26 -0
  28. ndev/linux/commands/restart.py +34 -0
  29. ndev/linux/commands/setup.py +113 -0
  30. ndev/linux/commands/start.py +34 -0
  31. ndev/linux/commands/status.py +81 -0
  32. ndev/linux/commands/stop.py +34 -0
  33. ndev/linux/commands/uninstall.py +69 -0
  34. ndev/linux/commands/update.py +57 -0
  35. ndev/linux/commands/upgrade.py +81 -0
  36. ndev/linux/commands/use.py +108 -0
  37. ndev/linux/commands/vhost.py +350 -0
  38. ndev/linux/php/builder.py +183 -0
  39. ndev/linux/php/downloader.py +58 -0
  40. ndev/linux/php/extensions.py +146 -0
  41. ndev/linux/php/installer.py +42 -0
  42. ndev/linux/php/resolver.py +59 -0
  43. ndev/linux/php/templates.py +128 -0
  44. ndev/linux/runtime/fpm.py +117 -0
  45. ndev/linux/runtime/mailpit.py +244 -0
  46. ndev/linux/runtime/pma.py +223 -0
  47. ndev/linux/runtime/process.py +37 -0
  48. ndev/linux/runtime/sockets.py +16 -0
  49. ndev/linux/runtime/upgrade.py +431 -0
  50. ndev/linux/tui.py +1423 -0
  51. ndev/main.py +52 -0
  52. ndev/tui.py +23 -0
  53. ndev/win/__init__.py +1 -0
  54. ndev/win/cli.py +1898 -0
  55. ndev/win/commands/__init__.py +0 -0
  56. ndev/win/core/__init__.py +0 -0
  57. ndev/win/core/db.py +265 -0
  58. ndev/win/core/elevate.py +94 -0
  59. ndev/win/core/ext.py +241 -0
  60. ndev/win/core/fcgi.py +216 -0
  61. ndev/win/core/grok.py +55 -0
  62. ndev/win/core/logs.py +66 -0
  63. ndev/win/core/mailpit.py +236 -0
  64. ndev/win/core/mkcert.py +65 -0
  65. ndev/win/core/paths.py +85 -0
  66. ndev/win/core/php.py +533 -0
  67. ndev/win/core/pma.py +190 -0
  68. ndev/win/core/services.py +349 -0
  69. ndev/win/core/setup.py +361 -0
  70. ndev/win/core/upgrade.py +513 -0
  71. ndev/win/core/vhost.py +289 -0
  72. ndev/win/templates/vhost.conf.tmpl +33 -0
  73. ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
  74. ndev/win/tui.py +1313 -0
  75. ndev_stack-0.1.0.dist-info/METADATA +553 -0
  76. ndev_stack-0.1.0.dist-info/RECORD +79 -0
  77. ndev_stack-0.1.0.dist-info/WHEEL +5 -0
  78. ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
  79. 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