ndev-stack 0.3.1__tar.gz → 0.3.2__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.2}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/__init__.py +1 -1
  3. ndev_stack-0.3.2/ndev/linux/commands/grok.py +97 -0
  4. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/cli.py +10 -3
  5. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/fcgi.py +7 -3
  6. ndev_stack-0.3.2/ndev/win/core/grok.py +95 -0
  7. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/vhost.py +9 -0
  8. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/templates/vhost.conf.tmpl +2 -0
  9. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/templates/vhost_ssl.conf.tmpl +2 -0
  10. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/PKG-INFO +1 -1
  11. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/pyproject.toml +1 -1
  12. ndev_stack-0.3.1/ndev/linux/commands/grok.py +0 -74
  13. ndev_stack-0.3.1/ndev/win/core/grok.py +0 -54
  14. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/README.md +0 -0
  15. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/__main__.py +0 -0
  16. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/cli.py +0 -0
  17. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/__init__.py +0 -0
  18. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/config.py +0 -0
  19. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/constants.py +0 -0
  20. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/github.py +0 -0
  21. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/logger.py +0 -0
  22. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/manifest.py +0 -0
  23. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/__init__.py +0 -0
  24. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/base.py +0 -0
  25. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  26. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  27. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  28. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  29. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  30. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/postgres/module.py +0 -0
  31. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  32. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/builtin/redis/module.py +0 -0
  33. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/modules/manager.py +0 -0
  34. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/common/utils.py +0 -0
  35. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/__init__.py +0 -0
  36. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/manager.py +0 -0
  37. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/packages.py +0 -0
  38. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/chroot/shell.py +0 -0
  39. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/cli.py +0 -0
  40. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/available.py +0 -0
  41. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/clean.py +0 -0
  42. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/ctl.py +0 -0
  43. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/current.py +0 -0
  44. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/db.py +0 -0
  45. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/doctor.py +0 -0
  46. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/install.py +0 -0
  47. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/list.py +0 -0
  48. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/logs.py +0 -0
  49. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/mailpit.py +0 -0
  50. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/modules.py +0 -0
  51. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/mongodb.py +0 -0
  52. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/postgres.py +0 -0
  53. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/reload.py +0 -0
  54. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/restart.py +0 -0
  55. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/setup.py +0 -0
  56. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/start.py +0 -0
  57. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/status.py +0 -0
  58. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/stop.py +0 -0
  59. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/uninstall.py +0 -0
  60. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/update.py +0 -0
  61. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/upgrade.py +0 -0
  62. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/use.py +0 -0
  63. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/commands/vhost.py +0 -0
  64. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/builder.py +0 -0
  65. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/downloader.py +0 -0
  66. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/extensions.py +0 -0
  67. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/installer.py +0 -0
  68. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/resolver.py +0 -0
  69. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/php/templates.py +0 -0
  70. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/fpm.py +0 -0
  71. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/mailpit.py +0 -0
  72. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/pma.py +0 -0
  73. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/process.py +0 -0
  74. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/sockets.py +0 -0
  75. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/runtime/upgrade.py +0 -0
  76. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/linux/tui.py +0 -0
  77. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/main.py +0 -0
  78. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/tui.py +0 -0
  79. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/__init__.py +0 -0
  80. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/bin/shim.exe +0 -0
  81. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/commands/__init__.py +0 -0
  82. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/__init__.py +0 -0
  83. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/db.py +0 -0
  84. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/elevate.py +0 -0
  85. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/ext.py +0 -0
  86. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/logs.py +0 -0
  87. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/mailpit.py +0 -0
  88. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/mkcert.py +0 -0
  89. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/paths.py +0 -0
  90. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/php.py +0 -0
  91. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/pma.py +0 -0
  92. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/redis_core.py +0 -0
  93. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/services.py +0 -0
  94. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/setup.py +0 -0
  95. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/shim.py +0 -0
  96. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/core/upgrade.py +0 -0
  97. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev/win/tui.py +0 -0
  98. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/SOURCES.txt +0 -0
  99. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/dependency_links.txt +0 -0
  100. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/entry_points.txt +0 -0
  101. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/requires.txt +0 -0
  102. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/ndev_stack.egg-info/top_level.txt +0 -0
  103. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/setup.cfg +0 -0
  104. {ndev_stack-0.3.1 → ndev_stack-0.3.2}/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.2
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.2"
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
+
@@ -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.2
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.2"
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