ndev-stack 0.3.8__tar.gz → 0.3.9__tar.gz

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 (107) hide show
  1. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/__init__.py +1 -1
  3. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/vhost.py +153 -79
  4. ndev_stack-0.3.9/ndev/linux/runtime/nginx_helper.py +482 -0
  5. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/tui.py +19 -28
  6. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/PKG-INFO +1 -1
  7. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/pyproject.toml +1 -1
  8. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/tests/test_opensuse_compat.py +74 -0
  9. ndev_stack-0.3.8/ndev/linux/runtime/nginx_helper.py +0 -269
  10. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/README.md +0 -0
  11. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/__main__.py +0 -0
  12. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/cli.py +0 -0
  13. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/__init__.py +0 -0
  14. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/config.py +0 -0
  15. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/constants.py +0 -0
  16. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/distro.py +0 -0
  17. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/github.py +0 -0
  18. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/logger.py +0 -0
  19. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/manifest.py +0 -0
  20. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/__init__.py +0 -0
  21. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/base.py +0 -0
  22. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  23. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  24. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  25. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  26. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  27. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/postgres/module.py +0 -0
  28. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  29. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/builtin/redis/module.py +0 -0
  30. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/modules/manager.py +0 -0
  31. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/common/utils.py +0 -0
  32. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/__init__.py +0 -0
  33. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/chroot/manager.py +0 -0
  34. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/chroot/packages.py +0 -0
  35. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/chroot/shell.py +0 -0
  36. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/cli.py +0 -0
  37. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/available.py +0 -0
  38. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/clean.py +0 -0
  39. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/ctl.py +0 -0
  40. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/current.py +0 -0
  41. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/db.py +0 -0
  42. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/doctor.py +0 -0
  43. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/grok.py +0 -0
  44. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/install.py +0 -0
  45. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/list.py +0 -0
  46. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/logs.py +0 -0
  47. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/mailpit.py +0 -0
  48. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/modules.py +0 -0
  49. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/mongodb.py +0 -0
  50. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/postgres.py +0 -0
  51. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/reload.py +0 -0
  52. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/restart.py +0 -0
  53. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/setup.py +0 -0
  54. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/start.py +0 -0
  55. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/status.py +0 -0
  56. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/stop.py +0 -0
  57. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/uninstall.py +0 -0
  58. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/update.py +0 -0
  59. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/upgrade.py +0 -0
  60. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/commands/use.py +0 -0
  61. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/builder.py +0 -0
  62. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/downloader.py +0 -0
  63. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/extensions.py +0 -0
  64. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/installer.py +0 -0
  65. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/resolver.py +0 -0
  66. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/php/templates.py +0 -0
  67. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/fpm.py +0 -0
  68. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/mailpit.py +0 -0
  69. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/pma.py +0 -0
  70. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/process.py +0 -0
  71. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/services.py +0 -0
  72. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/sockets.py +0 -0
  73. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/linux/runtime/upgrade.py +0 -0
  74. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/main.py +0 -0
  75. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/tui.py +0 -0
  76. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/__init__.py +0 -0
  77. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/bin/shim.exe +0 -0
  78. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/cli.py +0 -0
  79. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/commands/__init__.py +0 -0
  80. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/__init__.py +0 -0
  81. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/db.py +0 -0
  82. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/elevate.py +0 -0
  83. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/ext.py +0 -0
  84. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/fcgi.py +0 -0
  85. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/grok.py +0 -0
  86. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/logs.py +0 -0
  87. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/mailpit.py +0 -0
  88. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/mkcert.py +0 -0
  89. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/paths.py +0 -0
  90. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/php.py +0 -0
  91. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/pma.py +0 -0
  92. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/redis_core.py +0 -0
  93. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/services.py +0 -0
  94. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/setup.py +0 -0
  95. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/shim.py +0 -0
  96. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/upgrade.py +0 -0
  97. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/core/vhost.py +0 -0
  98. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/templates/vhost.conf.tmpl +0 -0
  99. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  100. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev/win/tui.py +0 -0
  101. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/SOURCES.txt +0 -0
  102. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/dependency_links.txt +0 -0
  103. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/entry_points.txt +0 -0
  104. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/requires.txt +0 -0
  105. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/ndev_stack.egg-info/top_level.txt +0 -0
  106. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/setup.cfg +0 -0
  107. {ndev_stack-0.3.8 → ndev_stack-0.3.9}/tests/test_shim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
5
5
  Author-email: Ankit Karan <ankit.karan99@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/ankitkaran99/ndev
@@ -1,7 +1,7 @@
1
1
  """
2
2
  ndev: Fast, cross-platform PHP developer environment for Windows and Linux.
3
3
  """
4
- __version__ = "0.3.8"
4
+ __version__ = "0.3.9"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -7,11 +7,14 @@ import typer
7
7
  from pathlib import Path
8
8
  from rich.console import Console
9
9
  from ndev.common.logger import logger
10
+ from ndev.common.utils import which_binary, resolve_installed_version
10
11
  from typing import Optional
11
12
 
12
13
  console = Console()
13
14
 
14
- def chown_to_sudo_user(path: Path):
15
+
16
+ def chown_to_sudo_user(path: Path) -> None:
17
+ """Change ownership of a path to the user who invoked sudo."""
15
18
  sudo_user = os.environ.get("SUDO_USER")
16
19
  if sudo_user:
17
20
  try:
@@ -21,39 +24,73 @@ def chown_to_sudo_user(path: Path):
21
24
  except Exception:
22
25
  pass
23
26
 
27
+
28
+ def find_mkcert_binary() -> Optional[str]:
29
+ """Find mkcert executable in system PATH or user local bin."""
30
+ found = which_binary("mkcert") or shutil.which("mkcert")
31
+ if found:
32
+ return found
33
+
34
+ candidates = [
35
+ Path.home() / ".local" / "bin" / "mkcert",
36
+ Path("/usr/local/bin/mkcert"),
37
+ Path("/usr/bin/mkcert"),
38
+ ]
39
+ sudo_user = os.environ.get("SUDO_USER")
40
+ if sudo_user:
41
+ try:
42
+ import pwd
43
+ user_home = Path(pwd.getpwnam(sudo_user).pw_dir)
44
+ candidates.insert(0, user_home / ".local" / "bin" / "mkcert")
45
+ except Exception:
46
+ pass
47
+
48
+ for c in candidates:
49
+ if c.exists() and os.access(c, os.X_OK):
50
+ return str(c)
51
+
52
+ return None
53
+
54
+
24
55
  def generate_local_cert(domain: str, certs_dir: Path) -> tuple[Path, Path]:
56
+ """Generate SSL certificate and private key using mkcert."""
25
57
  cert_path = certs_dir / f"{domain}.crt"
26
58
  key_path = certs_dir / f"{domain}.key"
27
-
59
+
28
60
  if cert_path.exists() and key_path.exists():
29
61
  return cert_path, key_path
30
-
62
+
31
63
  certs_dir.mkdir(parents=True, exist_ok=True)
32
64
  chown_to_sudo_user(certs_dir)
33
-
65
+
66
+ mkcert_bin = find_mkcert_binary()
67
+ if not mkcert_bin:
68
+ raise FileNotFoundError("mkcert binary not found in PATH or ~/.local/bin")
69
+
34
70
  sudo_user = os.environ.get("SUDO_USER")
35
- if sudo_user:
71
+ if sudo_user and os.geteuid() == 0:
36
72
  cmd = [
37
73
  "sudo", "-u", sudo_user,
38
- "mkcert",
74
+ mkcert_bin,
39
75
  "-cert-file", str(cert_path),
40
76
  "-key-file", str(key_path),
41
77
  domain, f"*.{domain}"
42
78
  ]
43
79
  else:
44
80
  cmd = [
45
- "mkcert",
81
+ mkcert_bin,
46
82
  "-cert-file", str(cert_path),
47
83
  "-key-file", str(key_path),
48
84
  domain, f"*.{domain}"
49
85
  ]
50
-
86
+
51
87
  subprocess.run(cmd, capture_output=True, text=True, check=True)
52
88
  chown_to_sudo_user(key_path)
53
89
  chown_to_sudo_user(cert_path)
54
-
90
+
55
91
  return cert_path, key_path
56
92
 
93
+
57
94
  def get_user_ndev_dir() -> Path:
58
95
  sudo_user = os.environ.get("SUDO_USER")
59
96
  if sudo_user:
@@ -64,6 +101,7 @@ def get_user_ndev_dir() -> Path:
64
101
  pass
65
102
  return Path(os.path.expanduser("~/.ndev"))
66
103
 
104
+
67
105
  def get_installed_php_versions() -> list[dict]:
68
106
  installed = []
69
107
  ndev_dir = get_user_ndev_dir()
@@ -72,7 +110,6 @@ def get_installed_php_versions() -> list[dict]:
72
110
  for path in php_dir.iterdir():
73
111
  if path.is_dir():
74
112
  ver = path.name
75
- # get major/minor for socket
76
113
  parts = ver.split(".")
77
114
  if len(parts) >= 2:
78
115
  mm = f"{parts[0]}{parts[1]}"
@@ -81,20 +118,19 @@ def get_installed_php_versions() -> list[dict]:
81
118
  mm = ver
82
119
  label = ver
83
120
  sock = ndev_dir / "run" / f"php{mm}.sock"
84
-
85
- # Check if it is running
121
+
86
122
  pid_file = ndev_dir / "run" / f"php-fpm-{mm}.pid"
87
123
  from ndev.linux.runtime.process import is_pid_running, read_pid_file
88
124
  pid = read_pid_file(pid_file)
89
125
  is_running = is_pid_running(pid) if pid else False
90
-
126
+
91
127
  installed.append({
92
128
  "version": ver,
93
129
  "label": label,
94
130
  "socket": sock,
95
131
  "running": is_running
96
132
  })
97
- # Sort by version
133
+
98
134
  from packaging.version import parse as parse_version
99
135
  try:
100
136
  installed.sort(key=lambda x: parse_version(x["version"]))
@@ -102,11 +138,14 @@ def get_installed_php_versions() -> list[dict]:
102
138
  installed.sort(key=lambda x: x["version"])
103
139
  return installed
104
140
 
141
+
105
142
  def vhost_cmd(
106
143
  domain: str = typer.Option(None, "--domain", "-d", help="Domain (e.g. project.local)"),
107
144
  root: str = typer.Option(None, "--root", "-r", help="Project Root Directory"),
108
145
  php: str = typer.Option(None, "--php", "-p", help="PHP socket alias, version, or index"),
109
- ssl: Optional[bool] = typer.Option(None, "--ssl/--no-ssl", help="Enable SSL/HTTPS with local certificate generation")
146
+ ssl: Optional[bool] = typer.Option(None, "--ssl/--no-ssl", help="Enable SSL/HTTPS with local certificate generation"),
147
+ cert: Optional[str] = typer.Option(None, "--cert", help="Path to existing SSL certificate file"),
148
+ key: Optional[str] = typer.Option(None, "--key", help="Path to existing SSL private key file"),
110
149
  ):
111
150
  """Create Nginx Virtual Host config, map to hosts file, and reload Nginx."""
112
151
  if not domain:
@@ -114,7 +153,7 @@ def vhost_cmd(
114
153
  if not domain:
115
154
  logger.error("Domain is required.")
116
155
  raise typer.Exit(code=1)
117
-
156
+
118
157
  if not root:
119
158
  sudo_user = os.environ.get("SUDO_USER")
120
159
  if sudo_user:
@@ -130,7 +169,7 @@ def vhost_cmd(
130
169
  if not root:
131
170
  logger.error("Project Root is required.")
132
171
  raise typer.Exit(code=1)
133
-
172
+
134
173
  root_path = Path(root).resolve()
135
174
  if not root_path.exists():
136
175
  root_path.mkdir(parents=True, exist_ok=True)
@@ -142,30 +181,52 @@ def vhost_cmd(
142
181
  encoding="utf-8"
143
182
  )
144
183
  chown_to_sudo_user(index_php)
145
-
184
+
146
185
  installed_phps = get_installed_php_versions()
147
186
  if not installed_phps:
148
187
  logger.error("No installed PHP versions found. Please install PHP using 'ndev install <version>' first.")
149
188
  raise typer.Exit(code=1)
150
-
189
+
151
190
  selected_sock = None
152
191
  selected_ver = None
153
192
  if php:
154
- # Check if it is a socket path
155
193
  php_path = Path(php)
156
- if php_path.exists() and php_path.suffix == ".sock":
194
+ if php_path.suffix == ".sock":
157
195
  selected_sock = php_path
196
+ # Find matching version from socket name
197
+ for item in installed_phps:
198
+ if item["socket"].name == php_path.name or item["socket"] == php_path:
199
+ selected_ver = item["version"]
200
+ break
201
+ if not selected_ver:
202
+ m = re.search(r"\d+", php_path.stem)
203
+ if m:
204
+ digits = m.group(0)
205
+ for item in installed_phps:
206
+ item_mm = item["version"].replace(".", "")[:len(digits)]
207
+ if item_mm == digits:
208
+ selected_ver = item["version"]
209
+ break
158
210
  else:
159
211
  clean_php = php.lower()
160
212
  if clean_php.startswith("ndev "):
161
213
  clean_php = clean_php[len("ndev "):]
162
-
214
+
163
215
  for item in installed_phps:
164
216
  if clean_php == item["label"].lower() or clean_php == item["version"].lower():
165
217
  selected_sock = item["socket"]
166
218
  selected_ver = item["version"]
167
219
  break
168
-
220
+
221
+ if not selected_sock:
222
+ resolved = resolve_installed_version(clean_php)
223
+ if resolved:
224
+ for item in installed_phps:
225
+ if item["version"] == resolved or item["label"] == resolved:
226
+ selected_sock = item["socket"]
227
+ selected_ver = item["version"]
228
+ break
229
+
169
230
  if not selected_sock:
170
231
  try:
171
232
  idx = int(php)
@@ -174,7 +235,7 @@ def vhost_cmd(
174
235
  selected_ver = installed_phps[idx - 1]["version"]
175
236
  except ValueError:
176
237
  pass
177
-
238
+
178
239
  if not selected_sock:
179
240
  logger.error(f"Invalid PHP selection: {php}")
180
241
  raise typer.Exit(code=1)
@@ -191,10 +252,10 @@ def vhost_cmd(
191
252
  raise typer.Exit(code=1)
192
253
  selected_sock = installed_phps[choice - 1]["socket"]
193
254
  selected_ver = installed_phps[choice - 1]["version"]
194
-
255
+
195
256
  if ssl is None:
196
257
  ssl = typer.confirm("Enable SSL/HTTPS?", default=False)
197
-
258
+
198
259
  # If selected PHP version is stopped, start it
199
260
  if selected_ver:
200
261
  selected_item = next((item for item in installed_phps if item["version"] == selected_ver), None)
@@ -205,22 +266,27 @@ def vhost_cmd(
205
266
  start_fpm(selected_ver)
206
267
  except Exception as e:
207
268
  logger.warning(f"Could not automatically start PHP-FPM: {e}")
208
-
269
+
209
270
  cert_path, key_path = None, None
210
271
  if ssl:
211
- if not shutil.which("mkcert"):
212
- logger.error("mkcert binary not found. Please install mkcert to generate local certificates.")
213
- raise typer.Exit(code=1)
214
- certs_dir = get_user_ndev_dir() / "certs"
215
- try:
216
- cert_path, key_path = generate_local_cert(domain, certs_dir)
217
- console.print("Generated SSL Certificates:")
218
- console.print(f" Cert: {cert_path}")
219
- console.print(f" Key : {key_path}")
220
- except Exception as e:
221
- logger.error(f"Failed to generate SSL certificate: {e}")
222
- raise typer.Exit(code=1)
223
-
272
+ if cert and key and Path(cert).exists() and Path(key).exists():
273
+ cert_path = Path(cert)
274
+ key_path = Path(key)
275
+ else:
276
+ mkcert_bin = find_mkcert_binary()
277
+ if not mkcert_bin:
278
+ logger.error("mkcert binary not found. Please install mkcert to generate local certificates ('ndev upgrade mkcert').")
279
+ raise typer.Exit(code=1)
280
+ certs_dir = get_user_ndev_dir() / "certs"
281
+ try:
282
+ cert_path, key_path = generate_local_cert(domain, certs_dir)
283
+ console.print("Generated SSL Certificates:")
284
+ console.print(f" Cert: {cert_path}")
285
+ console.print(f" Key : {key_path}")
286
+ except Exception as e:
287
+ logger.error(f"Failed to generate SSL certificate: {e}")
288
+ raise typer.Exit(code=1)
289
+
224
290
  if os.geteuid() != 0:
225
291
  console.print("\n[bold yellow]Privileged operations required. Elevating via sudo...[/bold yellow]")
226
292
  cmd = [
@@ -234,31 +300,44 @@ def vhost_cmd(
234
300
  "--root",
235
301
  root,
236
302
  "--php",
237
- str(selected_sock)
303
+ str(selected_sock),
238
304
  ]
239
305
  if ssl:
240
306
  cmd.append("--ssl")
307
+ if cert_path and key_path:
308
+ cmd.extend(["--cert", str(cert_path), "--key", str(key_path)])
241
309
  else:
242
310
  cmd.append("--no-ssl")
243
-
311
+
244
312
  try:
245
313
  res = subprocess.run(cmd)
246
314
  raise typer.Exit(code=res.returncode)
247
315
  except KeyboardInterrupt:
248
316
  raise typer.Exit(code=1)
249
-
250
- from ndev.linux.runtime.nginx_helper import get_nginx_vhost_dirs, generate_vhost_config
317
+
318
+ from ndev.linux.runtime.nginx_helper import (
319
+ get_nginx_vhost_dirs,
320
+ generate_vhost_config,
321
+ ensure_nginx_sites_enabled_included,
322
+ add_to_hosts_privileged,
323
+ reload_nginx,
324
+ )
251
325
 
252
326
  vhost_dir, enabled_dir = get_nginx_vhost_dirs()
253
- hosts_file = Path("/etc/hosts")
254
-
255
- if not vhost_dir.exists():
256
- try:
257
- vhost_dir.mkdir(parents=True, exist_ok=True)
258
- except Exception:
259
- logger.error(f"Nginx configuration directory not found and could not be created: {vhost_dir}")
260
- raise typer.Exit(code=1)
261
-
327
+
328
+ # Ensure target configuration directories exist
329
+ try:
330
+ vhost_dir.mkdir(parents=True, exist_ok=True)
331
+ if enabled_dir:
332
+ enabled_dir.mkdir(parents=True, exist_ok=True)
333
+ except Exception as e:
334
+ logger.error(f"Failed to create Nginx configuration directories: {e}")
335
+ raise typer.Exit(code=1)
336
+
337
+ # Ensure sites-enabled is bridged into nginx.conf if used
338
+ if enabled_dir:
339
+ ensure_nginx_sites_enabled_included()
340
+
262
341
  conf_file = vhost_dir / f"{domain}.conf"
263
342
  config_template = generate_vhost_config(
264
343
  domain=domain,
@@ -269,45 +348,39 @@ def vhost_cmd(
269
348
  key_path=key_path,
270
349
  )
271
350
 
351
+ enabled_link = None
272
352
  try:
273
- conf_file.write_text(config_template)
274
- enabled_link = None
353
+ conf_file.write_text(config_template, encoding="utf-8")
354
+
275
355
  if enabled_dir and enabled_dir != vhost_dir:
276
356
  enabled_link = enabled_dir / f"{domain}.conf"
277
- if enabled_link.exists() or enabled_link.is_symlink():
278
- enabled_link.unlink()
279
- enabled_link.symlink_to(conf_file)
280
-
281
- hosts_content = hosts_file.read_text()
282
- pattern = rf"^\s*127\.0\.0\.1\s+.*\b{re.escape(domain)}\b"
283
- found = False
284
- for line in hosts_content.splitlines():
285
- if re.match(pattern, line):
286
- found = True
287
- break
288
- if not found:
289
- with hosts_file.open("a") as f:
290
- f.write(f"\n127.0.0.1 {domain}\n")
291
-
292
- res = subprocess.run(["nginx", "-t"], capture_output=True, text=True)
357
+ enabled_link.unlink(missing_ok=True)
358
+ enabled_link.symlink_to(conf_file.resolve())
359
+
360
+ # Update /etc/hosts
361
+ add_to_hosts_privileged(domain)
362
+
363
+ # Test Nginx configuration before reloading
364
+ nginx_bin = which_binary("nginx") or shutil.which("nginx") or "nginx"
365
+ res = subprocess.run([nginx_bin, "-t"], capture_output=True, text=True)
293
366
  if res.returncode != 0:
294
367
  logger.error(f"Nginx config test failed:\n{res.stderr}")
295
368
  if enabled_link:
296
369
  enabled_link.unlink(missing_ok=True)
297
370
  conf_file.unlink(missing_ok=True)
298
371
  raise typer.Exit(code=1)
299
-
300
- if shutil.which("systemctl"):
301
- subprocess.run(["systemctl", "reload", "nginx"], check=True)
302
- elif shutil.which("service"):
303
- subprocess.run(["service", "nginx", "reload"], check=True)
304
-
372
+
373
+ # Reload Nginx service
374
+ reload_nginx()
375
+
305
376
  console.print("\n[bold green]VHost Created Successfully[/bold green]")
306
377
  console.print("--------------------------")
307
378
  console.print(f"Domain : {domain}")
308
379
  console.print(f"Root : {root}")
309
380
  console.print(f"PHP Socket : {selected_sock}")
310
381
  console.print(f"Config : {conf_file}")
382
+ if enabled_link:
383
+ console.print(f"Symlink : {enabled_link}")
311
384
  if ssl:
312
385
  console.print(f"SSL Cert : {cert_path}")
313
386
  console.print(f"SSL Key : {key_path}")
@@ -315,6 +388,9 @@ def vhost_cmd(
315
388
  else:
316
389
  console.print(f"\nOpen: http://{domain}")
317
390
  except Exception as e:
391
+ if enabled_link:
392
+ enabled_link.unlink(missing_ok=True)
393
+ conf_file.unlink(missing_ok=True)
318
394
  logger.error(f"Failed to create virtual host: {e}")
319
395
  raise typer.Exit(code=1)
320
396
 
@@ -382,5 +458,3 @@ def vhost_remove_cmd(
382
458
  console.print(f"[bold green]✓ Removed virtual host '{clean_domain}'.[/bold green]")
383
459
  else:
384
460
  console.print(f"[yellow]Virtual host '{clean_domain}' not found or could not be removed.[/yellow]")
385
-
386
-