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
ndev/win/core/vhost.py ADDED
@@ -0,0 +1,289 @@
1
+ """
2
+ Virtual host management for Windows:
3
+ - Writes Nginx server blocks under ~/.ndev/nginx/conf/ndev-vhosts/<domain>.conf
4
+ - Updates C:\\Windows\\System32\\drivers\\etc\\hosts (127.0.0.1)
5
+ - Generates local SSL certificates with mkcert
6
+ - Controls php-cgi FastCGI worker pool
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from . import fcgi, mkcert, paths
16
+ from .elevate import is_admin, run_elevated
17
+
18
+ TEMPLATE_PATH = Path(__file__).parent.parent / "templates" / "vhost.conf.tmpl"
19
+ SSL_TEMPLATE_PATH = Path(__file__).parent.parent / "templates" / "vhost_ssl.conf.tmpl"
20
+
21
+
22
+ def _load_templates() -> tuple[str, str]:
23
+ user_tmpl = paths.TEMPLATES_DIR / "vhost.conf.tmpl"
24
+ user_ssl_tmpl = paths.TEMPLATES_DIR / "vhost_ssl.conf.tmpl"
25
+
26
+ tmpl = (
27
+ user_tmpl.read_text(encoding="utf-8")
28
+ if user_tmpl.exists()
29
+ else TEMPLATE_PATH.read_text(encoding="utf-8")
30
+ )
31
+ ssl_tmpl = (
32
+ user_ssl_tmpl.read_text(encoding="utf-8")
33
+ if user_ssl_tmpl.exists()
34
+ else SSL_TEMPLATE_PATH.read_text(encoding="utf-8")
35
+ )
36
+ return tmpl, ssl_tmpl
37
+
38
+
39
+ def write_vhost_conf(domain: str, root: str | Path, php_version: str, ssl: bool = False) -> Path:
40
+ tmpl, ssl_tmpl = _load_templates()
41
+
42
+ upstream_name = fcgi.nginx_upstream_name(php_version, domain=domain)
43
+ upstream_block = fcgi.render_upstream_block(php_version, domain=domain)
44
+
45
+ root_clean = str(Path(root).resolve()).replace("\\", "/")
46
+ ndev_home = str(paths.NDEV_HOME.resolve()).replace("\\", "/")
47
+
48
+ # Ensure logs directory exists
49
+ paths.NGINX_LOGS_DIR.mkdir(parents=True, exist_ok=True)
50
+
51
+ if ssl:
52
+ cert_path, key_path = mkcert.generate_cert(domain)
53
+ conf = ssl_tmpl.format(
54
+ domain=domain,
55
+ root=root_clean,
56
+ ndev_home=ndev_home,
57
+ upstream_name=upstream_name,
58
+ upstream_block=upstream_block,
59
+ cert_path=cert_path,
60
+ key_path=key_path,
61
+ )
62
+ else:
63
+ conf = tmpl.format(
64
+ domain=domain,
65
+ root=root_clean,
66
+ ndev_home=ndev_home,
67
+ upstream_name=upstream_name,
68
+ upstream_block=upstream_block,
69
+ )
70
+
71
+ conf_header = f"# ndev-domain: {domain}\n# ndev-php: {php_version}\n"
72
+ full_conf = conf_header + conf
73
+
74
+ paths.NGINX_CONF_D.mkdir(parents=True, exist_ok=True)
75
+ conf_path = paths.NGINX_CONF_D / f"{domain}.conf"
76
+ conf_path.write_text(full_conf, encoding="utf-8")
77
+ return conf_path
78
+
79
+
80
+ def _ensure_writable(path: Path) -> None:
81
+ if path.exists():
82
+ import stat
83
+ try:
84
+ mode = path.stat().st_mode
85
+ if not (mode & stat.S_IWRITE):
86
+ path.chmod(mode | stat.S_IWRITE)
87
+ except Exception:
88
+ pass
89
+
90
+
91
+ def add_hosts_entry(domain: str) -> bool:
92
+ """
93
+ Add domain -> 127.0.0.1 mapping to Windows hosts file.
94
+ Uses direct write if admin, or elevated PowerShell if unprivileged.
95
+ """
96
+ domain = domain.strip().lower()
97
+ text = ""
98
+ if paths.HOSTS_PATH.exists():
99
+ text = paths.HOSTS_PATH.read_text(encoding="utf-8", errors="ignore")
100
+
101
+ for line in text.splitlines():
102
+ line = line.strip()
103
+ if not line or line.startswith("#"):
104
+ continue
105
+ parts = line.split()
106
+ if len(parts) >= 2 and domain in [p.lower() for p in parts[1:]]:
107
+ return False # Already mapped
108
+
109
+ if is_admin():
110
+ _ensure_writable(paths.HOSTS_PATH)
111
+ with paths.HOSTS_PATH.open("a", encoding="utf-8") as f:
112
+ f.write(f"\n127.0.0.1\t{domain}\n")
113
+ return True
114
+
115
+ entry_bytes = f"\r\n127.0.0.1\t{domain}\r\n".encode("utf-8")
116
+ b64_entry = base64.b64encode(entry_bytes).decode("ascii")
117
+ ps_script = (
118
+ f"Set-ItemProperty -Path '{paths.HOSTS_PATH}' -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue; "
119
+ f"$bytes = [System.Convert]::FromBase64String('{b64_entry}'); "
120
+ f"$stream = [System.IO.File]::Open('{paths.HOSTS_PATH}', [System.IO.FileMode]::Append); "
121
+ f"$stream.Write($bytes, 0, $bytes.Length); $stream.Close()"
122
+ )
123
+ encoded = base64.b64encode(ps_script.encode("utf-16le")).decode("ascii")
124
+ exit_code = run_elevated(["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded])
125
+ if exit_code != 0:
126
+ raise RuntimeError(f"Failed to update hosts file (exit code {exit_code})")
127
+ return True
128
+
129
+
130
+ def remove_hosts_entry(domain: str) -> bool:
131
+ """Remove domain from Windows hosts file."""
132
+ domain = domain.strip().lower()
133
+ if not paths.HOSTS_PATH.exists():
134
+ return False
135
+
136
+ text = paths.HOSTS_PATH.read_text(encoding="utf-8", errors="ignore")
137
+ lines = text.splitlines()
138
+ new_lines = []
139
+ removed = False
140
+
141
+ for line in lines:
142
+ stripped = line.strip()
143
+ if not stripped or stripped.startswith("#"):
144
+ new_lines.append(line)
145
+ continue
146
+ parts = stripped.split()
147
+ if len(parts) >= 2 and domain in [p.lower() for p in parts[1:]]:
148
+ remaining_hosts = [p for p in parts[1:] if p.lower() != domain]
149
+ if remaining_hosts:
150
+ new_lines.append(f"{parts[0]}\t" + " ".join(remaining_hosts))
151
+ removed = True
152
+ else:
153
+ new_lines.append(line)
154
+
155
+ if not removed:
156
+ return False
157
+
158
+ new_content = "\r\n".join(new_lines) + "\r\n"
159
+ if is_admin():
160
+ _ensure_writable(paths.HOSTS_PATH)
161
+ paths.HOSTS_PATH.write_text(new_content, encoding="utf-8")
162
+ return True
163
+
164
+ b64_content = base64.b64encode(new_content.encode("utf-8")).decode("ascii")
165
+ ps_script = (
166
+ f"Set-ItemProperty -Path '{paths.HOSTS_PATH}' -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue; "
167
+ f"$bytes = [System.Convert]::FromBase64String('{b64_content}'); "
168
+ f"[System.IO.File]::WriteAllBytes('{paths.HOSTS_PATH}', $bytes)"
169
+ )
170
+ encoded = base64.b64encode(ps_script.encode("utf-16le")).decode("ascii")
171
+ exit_code = run_elevated(["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded])
172
+ if exit_code != 0:
173
+ raise RuntimeError(f"Failed to update hosts file (exit code {exit_code})")
174
+ return True
175
+
176
+
177
+ def create_vhost(domain: str, root: str | Path, php_version: str, ssl: bool = False,
178
+ auto_start_pool: bool = True) -> Path:
179
+ """
180
+ Creates an Nginx virtual host, ensures php-cgi pool is running, and maps to hosts file.
181
+ """
182
+ root_path = Path(root).resolve()
183
+ if not root_path.exists():
184
+ root_path.mkdir(parents=True, exist_ok=True)
185
+ if not any(root_path.iterdir()):
186
+ (root_path / "index.php").write_text(
187
+ f"<?php\n"
188
+ f"// Virtual host: {domain}\n"
189
+ f"echo '<h1>Welcome to {domain}</h1>';\n"
190
+ f"echo '<p>Served by Nginx & PHP {php_version} via ndev</p>';\n"
191
+ f"phpinfo();\n",
192
+ encoding="utf-8"
193
+ )
194
+
195
+ # Local import to avoid circular dependencies
196
+ from . import php, services
197
+
198
+ try:
199
+ resolved_php = php.resolve_installed(php_version)
200
+ except Exception:
201
+ resolved_php = php_version
202
+
203
+ if not fcgi.status(resolved_php):
204
+ if not auto_start_pool:
205
+ raise RuntimeError(
206
+ f"PHP {resolved_php} pool is not running -- run "
207
+ f"`ndev pool start {resolved_php}` first, or pass auto_start_pool=True"
208
+ )
209
+ cfg = paths.load_config()
210
+ fcgi.start(resolved_php, php.php_cgi_exe(resolved_php),
211
+ cfg["fcgi_workers_per_version"], cfg["fcgi_base_port"])
212
+
213
+ conf_path = write_vhost_conf(domain, root_path, resolved_php, ssl=ssl)
214
+ add_hosts_entry(domain)
215
+
216
+ # If Nginx is installed and running, reload it
217
+ if (paths.NGINX_DIR / "nginx.exe").exists():
218
+ try:
219
+ services.nginx_reload()
220
+ except Exception:
221
+ pass
222
+
223
+ return conf_path
224
+
225
+
226
+ def remove_vhost(domain: str) -> bool:
227
+ """Remove a virtual host config and hosts file entry."""
228
+ clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
229
+ conf_path = paths.NGINX_CONF_D / f"{clean_domain}.conf"
230
+ cert_dir = paths.CERTS_DIR / clean_domain
231
+ if not conf_path.exists() and not cert_dir.exists():
232
+ return False
233
+
234
+ removed = False
235
+ if conf_path.exists():
236
+ conf_path.unlink()
237
+ removed = True
238
+ remove_hosts_entry(clean_domain)
239
+
240
+ # Clean up SSL cert directory if present
241
+ if cert_dir.exists():
242
+ import shutil
243
+ try:
244
+ shutil.rmtree(cert_dir)
245
+ except Exception:
246
+ pass
247
+
248
+ # If Nginx is installed and running, reload it
249
+ from . import services
250
+ if (paths.NGINX_DIR / "nginx.exe").exists():
251
+ try:
252
+ services.nginx_reload()
253
+ except Exception:
254
+ pass
255
+
256
+ return removed
257
+
258
+
259
+ def list_vhosts() -> list[dict]:
260
+ """List all configured virtual hosts."""
261
+ if not paths.NGINX_CONF_D.exists():
262
+ return []
263
+ vhosts = []
264
+ for p in sorted(paths.NGINX_CONF_D.glob("*.conf")):
265
+ if p.name.startswith("_"):
266
+ continue
267
+ domain = p.stem
268
+ content = p.read_text(encoding="utf-8", errors="ignore")
269
+ root_m = re.search(r'root\s+["\']?([^";\r\n]+?)["\']?;', content)
270
+ root = root_m.group(1).strip() if root_m else "N/A"
271
+
272
+ # Check explicit comment header first, then upstream pattern
273
+ m_comment = re.search(r'#\s*ndev-php:\s*([\w.\-]+)', content)
274
+ if m_comment:
275
+ php_ver = m_comment.group(1)
276
+ else:
277
+ clean_domain = re.sub(r"[^a-zA-Z0-9_]", "_", domain.lower())
278
+ m_up = re.search(rf'upstream\s+php_([\d_]+)_{clean_domain}\b', content) or re.search(r'upstream\s+php_([\d_]+)\b', content)
279
+ php_ver = m_up.group(1).replace("_", ".") if m_up else "unknown"
280
+
281
+ has_ssl = "listen 443 ssl" in content
282
+ vhosts.append({
283
+ "domain": domain,
284
+ "root": root,
285
+ "php": php_ver,
286
+ "ssl": has_ssl,
287
+ "conf": str(p),
288
+ })
289
+ return vhosts
@@ -0,0 +1,33 @@
1
+ {upstream_block}
2
+ server {{
3
+ listen 80;
4
+ server_name {domain};
5
+ root "{root}";
6
+ index index.php index.html index.htm;
7
+ client_max_body_size 128M;
8
+
9
+ access_log "{ndev_home}/nginx/logs/{domain}.access.log";
10
+ error_log "{ndev_home}/nginx/logs/{domain}.error.log";
11
+
12
+ location / {{
13
+ try_files $uri $uri/ /index.php?$query_string;
14
+ }}
15
+
16
+ location ~ \.php$ {{
17
+ fastcgi_pass {upstream_name};
18
+ fastcgi_index index.php;
19
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
20
+ include fastcgi_params;
21
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
22
+ fastcgi_param PATH_INFO $fastcgi_path_info;
23
+ fastcgi_buffer_size 32k;
24
+ fastcgi_buffers 16 16k;
25
+ fastcgi_busy_buffers_size 64k;
26
+ fastcgi_read_timeout 300;
27
+ fastcgi_send_timeout 300;
28
+ }}
29
+
30
+ location ~ /\.ht {{
31
+ deny all;
32
+ }}
33
+ }}
@@ -0,0 +1,43 @@
1
+
2
+ {upstream_block}
3
+ server {{
4
+ listen 80;
5
+ server_name {domain};
6
+ return 301 https://$host$request_uri;
7
+ }}
8
+
9
+ server {{
10
+ listen 443 ssl;
11
+ server_name {domain};
12
+ root "{root}";
13
+ index index.php index.html index.htm;
14
+ client_max_body_size 128M;
15
+
16
+ ssl_certificate "{cert_path}";
17
+ ssl_certificate_key "{key_path}";
18
+
19
+ access_log "{ndev_home}/nginx/logs/{domain}.ssl.access.log";
20
+ error_log "{ndev_home}/nginx/logs/{domain}.ssl.error.log";
21
+
22
+ location / {{
23
+ try_files $uri $uri/ /index.php?$query_string;
24
+ }}
25
+
26
+ location ~ \.php$ {{
27
+ fastcgi_pass {upstream_name};
28
+ fastcgi_index index.php;
29
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
30
+ include fastcgi_params;
31
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
32
+ fastcgi_param PATH_INFO $fastcgi_path_info;
33
+ fastcgi_buffer_size 32k;
34
+ fastcgi_buffers 16 16k;
35
+ fastcgi_busy_buffers_size 64k;
36
+ fastcgi_read_timeout 300;
37
+ fastcgi_send_timeout 300;
38
+ }}
39
+
40
+ location ~ /\.ht {{
41
+ deny all;
42
+ }}
43
+ }}