ndev-stack 0.3.1__tar.gz → 0.3.3__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 (104) hide show
  1. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/__init__.py +1 -1
  3. ndev_stack-0.3.3/ndev/linux/commands/grok.py +97 -0
  4. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/cli.py +10 -3
  5. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/fcgi.py +7 -3
  6. ndev_stack-0.3.3/ndev/win/core/grok.py +95 -0
  7. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/setup.py +56 -22
  8. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/vhost.py +9 -0
  9. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/templates/vhost.conf.tmpl +2 -0
  10. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/templates/vhost_ssl.conf.tmpl +2 -0
  11. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/PKG-INFO +1 -1
  12. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/pyproject.toml +1 -1
  13. ndev_stack-0.3.1/ndev/linux/commands/grok.py +0 -74
  14. ndev_stack-0.3.1/ndev/win/core/grok.py +0 -54
  15. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/README.md +0 -0
  16. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/__main__.py +0 -0
  17. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/cli.py +0 -0
  18. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/__init__.py +0 -0
  19. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/config.py +0 -0
  20. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/constants.py +0 -0
  21. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/github.py +0 -0
  22. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/logger.py +0 -0
  23. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/manifest.py +0 -0
  24. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/__init__.py +0 -0
  25. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/base.py +0 -0
  26. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  27. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  28. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  29. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  30. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  31. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/postgres/module.py +0 -0
  32. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  33. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/builtin/redis/module.py +0 -0
  34. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/modules/manager.py +0 -0
  35. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/common/utils.py +0 -0
  36. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/__init__.py +0 -0
  37. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/chroot/manager.py +0 -0
  38. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/chroot/packages.py +0 -0
  39. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/chroot/shell.py +0 -0
  40. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/cli.py +0 -0
  41. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/available.py +0 -0
  42. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/clean.py +0 -0
  43. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/ctl.py +0 -0
  44. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/current.py +0 -0
  45. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/db.py +0 -0
  46. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/doctor.py +0 -0
  47. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/install.py +0 -0
  48. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/list.py +0 -0
  49. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/logs.py +0 -0
  50. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/mailpit.py +0 -0
  51. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/modules.py +0 -0
  52. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/mongodb.py +0 -0
  53. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/postgres.py +0 -0
  54. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/reload.py +0 -0
  55. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/restart.py +0 -0
  56. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/setup.py +0 -0
  57. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/start.py +0 -0
  58. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/status.py +0 -0
  59. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/stop.py +0 -0
  60. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/uninstall.py +0 -0
  61. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/update.py +0 -0
  62. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/upgrade.py +0 -0
  63. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/use.py +0 -0
  64. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/commands/vhost.py +0 -0
  65. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/builder.py +0 -0
  66. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/downloader.py +0 -0
  67. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/extensions.py +0 -0
  68. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/installer.py +0 -0
  69. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/resolver.py +0 -0
  70. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/php/templates.py +0 -0
  71. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/fpm.py +0 -0
  72. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/mailpit.py +0 -0
  73. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/pma.py +0 -0
  74. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/process.py +0 -0
  75. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/sockets.py +0 -0
  76. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/runtime/upgrade.py +0 -0
  77. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/linux/tui.py +0 -0
  78. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/main.py +0 -0
  79. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/tui.py +0 -0
  80. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/__init__.py +0 -0
  81. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/bin/shim.exe +0 -0
  82. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/commands/__init__.py +0 -0
  83. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/__init__.py +0 -0
  84. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/db.py +0 -0
  85. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/elevate.py +0 -0
  86. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/ext.py +0 -0
  87. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/logs.py +0 -0
  88. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/mailpit.py +0 -0
  89. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/mkcert.py +0 -0
  90. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/paths.py +0 -0
  91. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/php.py +0 -0
  92. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/pma.py +0 -0
  93. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/redis_core.py +0 -0
  94. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/services.py +0 -0
  95. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/shim.py +0 -0
  96. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/core/upgrade.py +0 -0
  97. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev/win/tui.py +0 -0
  98. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/SOURCES.txt +0 -0
  99. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/dependency_links.txt +0 -0
  100. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/entry_points.txt +0 -0
  101. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/requires.txt +0 -0
  102. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/ndev_stack.egg-info/top_level.txt +0 -0
  103. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/setup.cfg +0 -0
  104. {ndev_stack-0.3.1 → ndev_stack-0.3.3}/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.1
3
+ Version: 0.3.3
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.1"
4
+ __version__ = "0.3.3"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -0,0 +1,97 @@
1
+ import tempfile
2
+ import subprocess
3
+ import shutil
4
+ import typer
5
+ from pathlib import Path
6
+ from rich.console import Console
7
+ from ndev.common.logger import logger
8
+
9
+ console = Console()
10
+
11
+ def get_vhost_info() -> list[dict]:
12
+ nginx_dir = Path("/etc/nginx/sites-enabled")
13
+ vhosts = []
14
+ if not nginx_dir.exists():
15
+ return vhosts
16
+
17
+ for file in nginx_dir.glob("*.conf"):
18
+ try:
19
+ content = file.read_text()
20
+ domain = None
21
+ for line in content.splitlines():
22
+ line = line.strip()
23
+ if line.startswith("server_name"):
24
+ parts = line.split()
25
+ for p in parts[1:]:
26
+ p = p.rstrip(";").strip()
27
+ if p and p != "_":
28
+ domain = p
29
+ break
30
+ if domain:
31
+ break
32
+ if domain:
33
+ has_ssl = "listen 443 ssl" in content or "listen [::]:443 ssl" in content
34
+ vhosts.append({"domain": domain, "ssl": has_ssl})
35
+ except Exception:
36
+ pass
37
+ return vhosts
38
+
39
+ def grok_cmd():
40
+ """Proxy local Nginx vhosts over public internet using Ngrok."""
41
+ if not shutil.which("ngrok"):
42
+ logger.error("ngrok is not installed or not in PATH.")
43
+ raise typer.Exit(code=1)
44
+
45
+ vhosts = get_vhost_info()
46
+ if not vhosts:
47
+ logger.error("No vhosts found in /etc/nginx/sites-enabled.")
48
+ raise typer.Exit(code=1)
49
+
50
+ console.print("\n[bold]Available VHosts[/bold]")
51
+ console.print("----------------")
52
+ for i, v in enumerate(vhosts):
53
+ ssl_tag = " (HTTPS)" if v["ssl"] else ""
54
+ console.print(f" {i + 1}) {v['domain']}{ssl_tag}")
55
+
56
+ console.print("")
57
+ choice = typer.prompt("Select vhost index", type=int)
58
+ if choice < 1 or choice > len(vhosts):
59
+ logger.error("Invalid selection.")
60
+ raise typer.Exit(code=1)
61
+
62
+ selected = vhosts[choice - 1]
63
+ domain = selected["domain"]
64
+ is_ssl = selected["ssl"]
65
+
66
+ policy_content = f'''on_http_request:
67
+ - actions:
68
+ - type: add-headers
69
+ config:
70
+ headers:
71
+ Host: "{domain}"
72
+
73
+ on_http_response:
74
+ - expressions:
75
+ - 'res.status_code >= 300 && res.status_code < 400 && "location" in res.headers'
76
+ - 'res.headers["location"][0].contains("://{domain}")'
77
+ actions:
78
+ - type: add-headers
79
+ config:
80
+ headers:
81
+ Location: '${{res.headers["location"][0].replace("https://{domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "").replace("http://{domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "")}}'
82
+ '''
83
+
84
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
85
+ tmp.write(policy_content)
86
+ policy_file = tmp.name
87
+
88
+ logger.info(f"Selected domain: {domain}{' (HTTPS)' if is_ssl else ''}")
89
+ logger.info("Starting ngrok...")
90
+ target = "https://localhost:443" if is_ssl else "80"
91
+ cmd = ["ngrok", "http", target, "--traffic-policy-file", policy_file]
92
+ if is_ssl:
93
+ cmd.append("--upstream-tls-verify=false")
94
+ try:
95
+ subprocess.run(cmd)
96
+ finally:
97
+ Path(policy_file).unlink(missing_ok=True)
@@ -1877,7 +1877,7 @@ def redis_cli(port: int):
1877
1877
 
1878
1878
  @main.command(name="grok")
1879
1879
  @click.option("--domain", default=None, help="Vhost domain to tunnel. Omit to select interactively.")
1880
- @click.option("--ssl", is_flag=True, help="Tunnel HTTPS port 443")
1880
+ @click.option("--ssl/--no-ssl", default=None, help="Tunnel HTTPS port 443 (defaults to auto-detecting vhost SSL)")
1881
1881
  def grok(domain, ssl):
1882
1882
  """Tunnel a local virtual host to the public web via ngrok."""
1883
1883
  if not domain:
@@ -1897,12 +1897,19 @@ def grok(domain, ssl):
1897
1897
  raise click.ClickException("Invalid selection.")
1898
1898
 
1899
1899
  domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
1900
- console.print(f"Starting ngrok tunnel for [bold cyan]{domain}[/bold cyan]{' (HTTPS)' if ssl else ''} -- Ctrl+C to stop.")
1901
- proc = grok_core.start_tunnel(domain, ssl=ssl)
1900
+ v_meta = vhost_core.get_vhost(domain)
1901
+ is_ssl = ssl if ssl is not None else bool(v_meta and v_meta.get("ssl"))
1902
+ console.print(f"Starting ngrok tunnel for [bold cyan]{domain}[/bold cyan]{' (HTTPS)' if is_ssl else ''} -- Ctrl+C to stop.")
1903
+ proc, policy_file = grok_core.start_tunnel(domain, ssl=ssl)
1902
1904
  try:
1903
1905
  proc.wait()
1904
1906
  except KeyboardInterrupt:
1905
1907
  proc.terminate()
1908
+ finally:
1909
+ try:
1910
+ policy_file.unlink(missing_ok=True)
1911
+ except Exception:
1912
+ pass
1906
1913
 
1907
1914
 
1908
1915
  @main.command(name="tunnel", hidden=True)
@@ -209,8 +209,12 @@ def nginx_upstream_name(version: str, domain: str | None = None) -> str:
209
209
 
210
210
  def render_upstream_block(version: str, domain: str | None = None) -> str:
211
211
  workers = status(version)
212
- if not workers:
213
- raise RuntimeError(f"PHP {version} pool is not running; start it first")
214
- servers = "\n".join(f" server 127.0.0.1:{w.port};" for w in workers)
212
+ if workers:
213
+ servers = "\n".join(f" server 127.0.0.1:{w.port};" for w in workers)
214
+ else:
215
+ cfg = paths.load_config()
216
+ ports = ports_for(version, cfg["fcgi_workers_per_version"], cfg["fcgi_base_port"])
217
+ servers = "\n".join(f" server 127.0.0.1:{p};" for p in ports)
215
218
  u_name = nginx_upstream_name(version, domain=domain)
216
219
  return f"upstream {u_name} {{\n{servers}\n}}\n"
220
+
@@ -0,0 +1,95 @@
1
+ """
2
+ ngrok tunnel wrapper for ndev-win virtual hosts.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from . import paths, vhost
12
+
13
+ HTTP_PORT = 80
14
+ HTTPS_PORT = 443
15
+
16
+
17
+ def list_vhosts() -> list[str]:
18
+ """Return list of domain names with configured Nginx virtual hosts."""
19
+ return [v["domain"] for v in vhost.list_vhosts()]
20
+
21
+
22
+ def _ngrok_exe() -> str:
23
+ # 1. Check shim dir
24
+ shim_exe = paths.SHIM_DIR / "ngrok.exe"
25
+ if shim_exe.exists():
26
+ return str(shim_exe)
27
+
28
+ # 2. Check config
29
+ cfg = paths.load_config()
30
+ path = cfg.get("ngrok_path")
31
+ if path and Path(path).exists():
32
+ return str(path)
33
+
34
+ # 3. Check system PATH
35
+ which_path = shutil.which("ngrok") or shutil.which("ngrok.exe")
36
+ if which_path:
37
+ return which_path
38
+
39
+ raise FileNotFoundError("ngrok isn't installed -- run `ndev setup` first")
40
+
41
+
42
+ def start_tunnel(domain: str, ssl: bool | None = None) -> tuple[subprocess.Popen, Path]:
43
+ """
44
+ Start `ngrok http` against Nginx.
45
+ Rewrites the request Host header to `domain` and rewrites any response
46
+ redirect Location headers from the local domain back to the ngrok public URL.
47
+ Returns (Popen, policy_file_path).
48
+ """
49
+ import re
50
+ clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
51
+ vhost_meta = vhost.get_vhost(clean_domain)
52
+ known = list_vhosts()
53
+ if not vhost_meta and clean_domain not in known:
54
+ raise FileNotFoundError(
55
+ f"No vhost found for '{clean_domain}'. Known vhosts: {known or '(none)'}"
56
+ )
57
+
58
+ # Determine SSL mode: use explicit flag if provided, otherwise auto-detect from vhost config
59
+ is_ssl = ssl if ssl is not None else bool(vhost_meta and vhost_meta.get("ssl"))
60
+
61
+ target = f"https://localhost:{HTTPS_PORT}" if is_ssl else str(HTTP_PORT)
62
+
63
+ # Build traffic policy YAML to ensure:
64
+ # 1. Request Host header is rewritten to clean_domain so Nginx routes to the right vhost.
65
+ # 2. Response Location header rewriting prevents external devices from being redirected
66
+ # to the unresolvable local domain (e.g. *.local).
67
+ policy_content = f'''on_http_request:
68
+ - actions:
69
+ - type: add-headers
70
+ config:
71
+ headers:
72
+ Host: "{clean_domain}"
73
+
74
+ on_http_response:
75
+ - expressions:
76
+ - 'res.status_code >= 300 && res.status_code < 400 && "location" in res.headers'
77
+ - 'res.headers["location"][0].contains("://{clean_domain}")'
78
+ actions:
79
+ - type: add-headers
80
+ config:
81
+ headers:
82
+ Location: '${{res.headers["location"][0].replace("https://{clean_domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "").replace("http://{clean_domain}", conn.server_name.size() > 0 ? "https://" + conn.server_name : "")}}'
83
+ '''
84
+
85
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
86
+ tmp.write(policy_content)
87
+ policy_file = Path(tmp.name)
88
+
89
+ cmd = [_ngrok_exe(), "http", target, "--traffic-policy-file", str(policy_file)]
90
+ if is_ssl:
91
+ cmd.append("--upstream-tls-verify=false")
92
+
93
+ proc = subprocess.Popen(cmd)
94
+ return proc, policy_file
95
+
@@ -113,7 +113,7 @@ def install_nginx(version: str = DEFAULT_NGINX_VERSION) -> Path:
113
113
 
114
114
 
115
115
  def _include_vhosts_in_main_conf() -> None:
116
- """Add `include ndev-vhosts/*.conf;` inside the http{} block in nginx.conf."""
116
+ """Ensure vhost includes, proxy mapping maps, and fastcgi_params are configured."""
117
117
  # Ensure default fallback vhost exists so Nginx never fails wildcard glob
118
118
  default_conf = paths.NGINX_CONF_D / "_default.conf"
119
119
  if not default_conf.exists():
@@ -127,27 +127,61 @@ def _include_vhosts_in_main_conf() -> None:
127
127
  )
128
128
 
129
129
  conf = paths.NGINX_DIR / "conf" / "nginx.conf"
130
- if not conf.exists():
131
- return
132
- text = conf.read_text(encoding="utf-8", errors="ignore")
133
- include_line = " include ndev-vhosts/*.conf;\n"
134
- if "ndev-vhosts" in text:
135
- return
136
-
137
- http_start = text.find("http {")
138
- if http_start == -1:
139
- return
140
- brace_depth = 0
141
- i = text.find("{", http_start)
142
- for idx in range(i, len(text)):
143
- if text[idx] == "{":
144
- brace_depth += 1
145
- elif text[idx] == "}":
146
- brace_depth -= 1
147
- if brace_depth == 0:
148
- text = text[:idx] + include_line + text[idx:]
149
- break
150
- conf.write_text(text, encoding="utf-8")
130
+ if conf.exists():
131
+ text = conf.read_text(encoding="utf-8", errors="ignore")
132
+ changed = False
133
+
134
+ # Add forwarded header map directives if not already present
135
+ if "$fcgi_host" not in text:
136
+ map_block = (
137
+ "\n # Map forwarded headers from reverse proxies (e.g. ngrok) to FastCGI params\n"
138
+ " map $http_x_forwarded_host $fcgi_host {\n"
139
+ " default $http_x_forwarded_host;\n"
140
+ ' "" $host;\n'
141
+ " }\n\n"
142
+ " map $http_x_forwarded_proto $fcgi_scheme {\n"
143
+ " default $http_x_forwarded_proto;\n"
144
+ ' "" $scheme;\n'
145
+ " }\n\n"
146
+ " map $http_x_forwarded_proto $fcgi_https {\n"
147
+ ' default "on";\n'
148
+ ' "" $https;\n'
149
+ " }\n\n"
150
+ )
151
+ http_start = text.find("http {")
152
+ if http_start != -1:
153
+ brace_pos = text.find("{", http_start)
154
+ text = text[:brace_pos + 1] + map_block + text[brace_pos + 1:]
155
+ changed = True
156
+
157
+ if "ndev-vhosts" not in text:
158
+ include_line = " include ndev-vhosts/*.conf;\n"
159
+ http_start = text.find("http {")
160
+ if http_start != -1:
161
+ brace_depth = 0
162
+ i = text.find("{", http_start)
163
+ for idx in range(i, len(text)):
164
+ if text[idx] == "{":
165
+ brace_depth += 1
166
+ elif text[idx] == "}":
167
+ brace_depth -= 1
168
+ if brace_depth == 0:
169
+ text = text[:idx] + include_line + text[idx:]
170
+ changed = True
171
+ break
172
+
173
+ if changed:
174
+ conf.write_text(text, encoding="utf-8")
175
+
176
+ # Update fastcgi_params to forward $fcgi_host and $fcgi_scheme
177
+ fcgi_params_path = paths.NGINX_DIR / "conf" / "fastcgi_params"
178
+ if fcgi_params_path.exists():
179
+ fcgi_text = fcgi_params_path.read_text(encoding="utf-8", errors="ignore")
180
+ if "HTTP_HOST" not in fcgi_text or "$fcgi_host" not in fcgi_text:
181
+ fcgi_text = fcgi_text.replace("fastcgi_param REQUEST_SCHEME $scheme;", "fastcgi_param REQUEST_SCHEME $fcgi_scheme;")
182
+ fcgi_text = fcgi_text.replace("fastcgi_param HTTPS $https if_not_empty;", "fastcgi_param HTTPS $fcgi_https if_not_empty;")
183
+ fcgi_text = fcgi_text.replace("fastcgi_param SERVER_NAME $server_name;", "fastcgi_param SERVER_NAME $fcgi_host;\nfastcgi_param HTTP_HOST $fcgi_host;")
184
+ fcgi_params_path.write_text(fcgi_text, encoding="utf-8")
151
185
 
152
186
 
153
187
  # ---- MariaDB ------------------------------------------------------------------
@@ -286,3 +286,12 @@ def list_vhosts() -> list[dict]:
286
286
  "conf": str(p),
287
287
  })
288
288
  return vhosts
289
+
290
+
291
+ def get_vhost(domain: str) -> dict | None:
292
+ """Get virtual host metadata for a specific domain name."""
293
+ clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
294
+ for v in list_vhosts():
295
+ if v["domain"].lower() == clean_domain:
296
+ return v
297
+ return None
@@ -20,6 +20,8 @@ server {{
20
20
  include fastcgi_params;
21
21
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
22
22
  fastcgi_param PATH_INFO $fastcgi_path_info;
23
+ fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host if_not_empty;
24
+ fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty;
23
25
  fastcgi_buffer_size 32k;
24
26
  fastcgi_buffers 16 16k;
25
27
  fastcgi_busy_buffers_size 64k;
@@ -30,6 +30,8 @@ server {{
30
30
  include fastcgi_params;
31
31
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
32
32
  fastcgi_param PATH_INFO $fastcgi_path_info;
33
+ fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host if_not_empty;
34
+ fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty;
33
35
  fastcgi_buffer_size 32k;
34
36
  fastcgi_buffers 16 16k;
35
37
  fastcgi_busy_buffers_size 64k;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ndev-stack"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,74 +0,0 @@
1
- import tempfile
2
- import subprocess
3
- import shutil
4
- import typer
5
- from pathlib import Path
6
- from rich.console import Console
7
- from ndev.common.logger import logger
8
-
9
- console = Console()
10
-
11
- def get_vhosts() -> list[str]:
12
- nginx_dir = Path("/etc/nginx/sites-enabled")
13
- domains = []
14
- if not nginx_dir.exists():
15
- return domains
16
-
17
- for file in nginx_dir.glob("*.conf"):
18
- try:
19
- content = file.read_text()
20
- for line in content.splitlines():
21
- line = line.strip()
22
- if line.startswith("server_name"):
23
- parts = line.split()
24
- for p in parts[1:]:
25
- p = p.rstrip(";").strip()
26
- if p and p != "_":
27
- domains.append(p)
28
- break
29
- except Exception:
30
- pass
31
- return domains
32
-
33
- def grok_cmd():
34
- """Proxy local Nginx vhosts over public internet using Ngrok."""
35
- if not shutil.which("ngrok"):
36
- logger.error("ngrok is not installed or not in PATH.")
37
- raise typer.Exit(code=1)
38
-
39
- domains = get_vhosts()
40
- if not domains:
41
- logger.error("No vhosts found in /etc/nginx/sites-enabled.")
42
- raise typer.Exit(code=1)
43
-
44
- console.print("\n[bold]Available VHosts[/bold]")
45
- console.print("----------------")
46
- for i, d in enumerate(domains):
47
- console.print(f" {i + 1}) {d}")
48
-
49
- console.print("")
50
- choice = typer.prompt("Select vhost index", type=int)
51
- if choice < 1 or choice > len(domains):
52
- logger.error("Invalid selection.")
53
- raise typer.Exit(code=1)
54
-
55
- domain = domains[choice - 1]
56
-
57
- policy_content = f"""on_http_request:
58
- - actions:
59
- - type: add-headers
60
- config:
61
- headers:
62
- Host: {domain}
63
- """
64
-
65
- with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
66
- tmp.write(policy_content)
67
- policy_file = tmp.name
68
-
69
- logger.info(f"Selected domain: {domain}")
70
- logger.info("Starting ngrok...")
71
- try:
72
- subprocess.run(["ngrok", "http", "80", "--traffic-policy-file", policy_file])
73
- finally:
74
- Path(policy_file).unlink(missing_ok=True)
@@ -1,54 +0,0 @@
1
- """
2
- ngrok tunnel wrapper for ndev-win virtual hosts.
3
- """
4
- from __future__ import annotations
5
-
6
- import shutil
7
- import subprocess
8
- from pathlib import Path
9
-
10
- from . import paths, vhost
11
-
12
- HTTP_PORT = 80
13
- HTTPS_PORT = 443
14
-
15
-
16
- def list_vhosts() -> list[str]:
17
- """Return list of domain names with configured Nginx virtual hosts."""
18
- return [v["domain"] for v in vhost.list_vhosts()]
19
-
20
-
21
- def _ngrok_exe() -> str:
22
- # 1. Check shim dir
23
- shim_exe = paths.SHIM_DIR / "ngrok.exe"
24
- if shim_exe.exists():
25
- return str(shim_exe)
26
-
27
- # 2. Check config
28
- cfg = paths.load_config()
29
- path = cfg.get("ngrok_path")
30
- if path and Path(path).exists():
31
- return str(path)
32
-
33
- # 3. Check system PATH
34
- which_path = shutil.which("ngrok") or shutil.which("ngrok.exe")
35
- if which_path:
36
- return which_path
37
-
38
- raise FileNotFoundError("ngrok isn't installed -- run `ndev setup` first")
39
-
40
-
41
- def start_tunnel(domain: str, ssl: bool = False) -> subprocess.Popen:
42
- """
43
- Start `ngrok http` against Nginx, rewriting Host header to `domain`.
44
- """
45
- import re
46
- clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
47
- known = list_vhosts()
48
- if clean_domain not in known:
49
- raise FileNotFoundError(
50
- f"No vhost found for '{clean_domain}'. Known vhosts: {known or '(none)'}"
51
- )
52
- target = f"https://localhost:{HTTPS_PORT}" if ssl else str(HTTP_PORT)
53
- cmd = [_ngrok_exe(), "http", target, f"--host-header={clean_domain}"]
54
- return subprocess.Popen(cmd)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes