ndev-stack 0.3.4__tar.gz → 0.3.6__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 (106) hide show
  1. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/__init__.py +1 -1
  3. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/utils.py +35 -0
  4. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/doctor.py +9 -8
  5. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/setup.py +20 -0
  6. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/upgrade.py +4 -4
  7. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/services.py +11 -4
  8. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/upgrade.py +57 -26
  9. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/tui.py +3 -2
  10. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/PKG-INFO +1 -1
  11. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/pyproject.toml +1 -1
  12. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/tests/test_opensuse_compat.py +12 -1
  13. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/README.md +0 -0
  14. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/__main__.py +0 -0
  15. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/cli.py +0 -0
  16. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/__init__.py +0 -0
  17. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/config.py +0 -0
  18. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/constants.py +0 -0
  19. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/distro.py +0 -0
  20. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/github.py +0 -0
  21. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/logger.py +0 -0
  22. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/manifest.py +0 -0
  23. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/__init__.py +0 -0
  24. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/base.py +0 -0
  25. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  26. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  27. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  28. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  29. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  30. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/postgres/module.py +0 -0
  31. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  32. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/builtin/redis/module.py +0 -0
  33. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/common/modules/manager.py +0 -0
  34. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/__init__.py +0 -0
  35. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/chroot/manager.py +0 -0
  36. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/chroot/packages.py +0 -0
  37. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/chroot/shell.py +0 -0
  38. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/cli.py +0 -0
  39. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/available.py +0 -0
  40. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/clean.py +0 -0
  41. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/ctl.py +0 -0
  42. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/current.py +0 -0
  43. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/db.py +0 -0
  44. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/grok.py +0 -0
  45. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/install.py +0 -0
  46. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/list.py +0 -0
  47. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/logs.py +0 -0
  48. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/mailpit.py +0 -0
  49. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/modules.py +0 -0
  50. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/mongodb.py +0 -0
  51. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/postgres.py +0 -0
  52. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/reload.py +0 -0
  53. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/restart.py +0 -0
  54. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/start.py +0 -0
  55. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/status.py +0 -0
  56. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/stop.py +0 -0
  57. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/uninstall.py +0 -0
  58. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/update.py +0 -0
  59. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/use.py +0 -0
  60. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/commands/vhost.py +0 -0
  61. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/builder.py +0 -0
  62. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/downloader.py +0 -0
  63. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/extensions.py +0 -0
  64. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/installer.py +0 -0
  65. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/resolver.py +0 -0
  66. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/php/templates.py +0 -0
  67. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/fpm.py +0 -0
  68. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/mailpit.py +0 -0
  69. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/nginx_helper.py +0 -0
  70. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/pma.py +0 -0
  71. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/process.py +0 -0
  72. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/linux/runtime/sockets.py +0 -0
  73. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/main.py +0 -0
  74. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/tui.py +0 -0
  75. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/__init__.py +0 -0
  76. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/bin/shim.exe +0 -0
  77. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/cli.py +0 -0
  78. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/commands/__init__.py +0 -0
  79. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/__init__.py +0 -0
  80. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/db.py +0 -0
  81. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/elevate.py +0 -0
  82. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/ext.py +0 -0
  83. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/fcgi.py +0 -0
  84. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/grok.py +0 -0
  85. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/logs.py +0 -0
  86. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/mailpit.py +0 -0
  87. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/mkcert.py +0 -0
  88. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/paths.py +0 -0
  89. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/php.py +0 -0
  90. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/pma.py +0 -0
  91. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/redis_core.py +0 -0
  92. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/services.py +0 -0
  93. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/setup.py +0 -0
  94. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/shim.py +0 -0
  95. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/upgrade.py +0 -0
  96. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/core/vhost.py +0 -0
  97. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/templates/vhost.conf.tmpl +0 -0
  98. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  99. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev/win/tui.py +0 -0
  100. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/SOURCES.txt +0 -0
  101. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/dependency_links.txt +0 -0
  102. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/entry_points.txt +0 -0
  103. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/requires.txt +0 -0
  104. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/ndev_stack.egg-info/top_level.txt +0 -0
  105. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/setup.cfg +0 -0
  106. {ndev_stack-0.3.4 → ndev_stack-0.3.6}/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.4
3
+ Version: 0.3.6
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.4"
4
+ __version__ = "0.3.6"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -94,3 +94,38 @@ def get_version_or_prompt(version: str = None, prompt_message: str = "PHP versio
94
94
 
95
95
  return typer.prompt(prompt_message).strip()
96
96
 
97
+ def which_binary(cmd: str) -> str | None:
98
+ """
99
+ Find executable in PATH plus standard system admin/daemon directories
100
+ (/usr/sbin, /sbin, /usr/local/sbin, ~/.local/bin, ~/.ndev/bin).
101
+ Essential on Linux where daemons (e.g. nginx, mysqld) are in /usr/sbin,
102
+ which is not in standard non-root user PATH.
103
+ """
104
+ if not cmd:
105
+ return None
106
+ import shutil
107
+ import os
108
+ from pathlib import Path
109
+ from ndev.common.constants import NDEV_DIR
110
+
111
+ found = shutil.which(cmd)
112
+ if found:
113
+ return found
114
+
115
+ p = Path(cmd)
116
+ if p.is_file() and os.access(p, os.X_OK):
117
+ return str(p.resolve())
118
+
119
+ extra_dirs = [
120
+ "/usr/sbin",
121
+ "/sbin",
122
+ "/usr/local/sbin",
123
+ "/usr/bin",
124
+ "/bin",
125
+ "/usr/local/bin",
126
+ str(Path.home() / ".local" / "bin"),
127
+ str(NDEV_DIR / "bin"),
128
+ ]
129
+ path_env = os.environ.get("PATH", "")
130
+ extended_path = ":".join([path_env] + extra_dirs) if path_env else ":".join(extra_dirs)
131
+ return shutil.which(cmd, path=extended_path)
@@ -4,6 +4,7 @@ from rich.console import Console
4
4
  from rich.table import Table
5
5
  from ndev.common.constants import NDEV_DIR, CONFIG_FILE
6
6
  from ndev.common.distro import get_distro_summary, get_pkg_manager
7
+ from ndev.common.utils import which_binary
7
8
 
8
9
  console = Console()
9
10
 
@@ -27,47 +28,47 @@ def doctor_cmd():
27
28
  build_install_cmd = "sudo apt install -y bubblewrap build-essential pkg-config"
28
29
  stack_install_cmd = "sudo apt install -y mariadb-server nginx curl"
29
30
 
30
- bwrap_path = shutil.which("bwrap")
31
+ bwrap_path = which_binary("bwrap")
31
32
  if bwrap_path:
32
33
  try:
33
- res = subprocess.run(["bwrap", "--version"], capture_output=True, text=True)
34
+ res = subprocess.run([bwrap_path, "--version"], capture_output=True, text=True)
34
35
  table.add_row("Bubblewrap (bwrap)", "[green]OK[/green]", f"Found at {bwrap_path} ({res.stdout.strip()})")
35
36
  except Exception as e:
36
37
  table.add_row("Bubblewrap (bwrap)", "[red]FAILED[/red]", f"Found at {bwrap_path} but failed to run: {e}")
37
38
  else:
38
39
  table.add_row("Bubblewrap (bwrap)", "[red]MISSING[/red]", f"Required for PHP sandbox. Install via: {build_install_cmd}")
39
40
 
40
- gcc_path = shutil.which("gcc")
41
+ gcc_path = which_binary("gcc")
41
42
  if gcc_path:
42
43
  table.add_row("GCC Compiler", "[green]OK[/green]", f"Found at {gcc_path}")
43
44
  else:
44
45
  table.add_row("GCC Compiler", "[red]MISSING[/red]", f"gcc is required to compile PHP. Install via: {build_install_cmd}")
45
46
 
46
- make_path = shutil.which("make")
47
+ make_path = which_binary("make")
47
48
  if make_path:
48
49
  table.add_row("Make Utility", "[green]OK[/green]", f"Found at {make_path}")
49
50
  else:
50
51
  table.add_row("Make Utility", "[red]MISSING[/red]", f"make is required to build PHP. Install via: {build_install_cmd}")
51
52
 
52
- pkgconfig_path = shutil.which("pkg-config")
53
+ pkgconfig_path = which_binary("pkg-config")
53
54
  if pkgconfig_path:
54
55
  table.add_row("Pkg-Config Utility", "[green]OK[/green]", f"Found at {pkgconfig_path}")
55
56
  else:
56
57
  table.add_row("Pkg-Config Utility", "[red]MISSING[/red]", f"pkg-config is required to find libraries. Install via: {build_install_cmd}")
57
58
 
58
- nginx_path = shutil.which("nginx")
59
+ nginx_path = which_binary("nginx")
59
60
  if nginx_path:
60
61
  table.add_row("Nginx Web Server", "[green]OK[/green]", f"Found at {nginx_path}")
61
62
  else:
62
63
  table.add_row("Nginx Web Server", "[yellow]NOT INSTALLED[/yellow]", f"Run 'ndev setup' or: {stack_install_cmd}")
63
64
 
64
- db_path = shutil.which("mariadb") or shutil.which("mysql") or shutil.which("mysqld")
65
+ db_path = which_binary("mariadb") or which_binary("mysql") or which_binary("mysqld")
65
66
  if db_path:
66
67
  table.add_row("MariaDB / MySQL", "[green]OK[/green]", f"Found at {db_path}")
67
68
  else:
68
69
  table.add_row("MariaDB / MySQL", "[yellow]NOT INSTALLED[/yellow]", f"Run 'ndev setup' or: {stack_install_cmd}")
69
70
 
70
- mkcert_path = shutil.which("mkcert")
71
+ mkcert_path = which_binary("mkcert")
71
72
  if mkcert_path:
72
73
  table.add_row("mkcert Local SSL", "[green]OK[/green]", f"Found at {mkcert_path}")
73
74
  else:
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from rich.console import Console
9
9
  from ndev.common.constants import CURRENT_LINK
10
10
  from ndev.common.logger import logger
11
+ from ndev.common.utils import which_binary
11
12
 
12
13
  console = Console()
13
14
 
@@ -55,6 +56,15 @@ def setup_cmd():
55
56
  packages = ["mariadb", "mariadb-client", "nginx", "curl", "bubblewrap"]
56
57
  subprocess.run(["sudo", "zypper", "--non-interactive", "install", "-y"] + packages, check=True)
57
58
 
59
+ # Optional mkcert package
60
+ try:
61
+ subprocess.run(["sudo", "zypper", "--non-interactive", "install", "-y", "mkcert"], check=False, capture_output=True)
62
+ mkcert_bin = which_binary("mkcert")
63
+ if mkcert_bin:
64
+ subprocess.run([mkcert_bin, "-install"], check=False, capture_output=True)
65
+ except Exception:
66
+ pass
67
+
58
68
  try:
59
69
  subprocess.run(["sudo", "systemctl", "enable", "--now", "mariadb"], check=False, capture_output=True)
60
70
  subprocess.run(["sudo", "systemctl", "enable", "--now", "nginx"], check=False, capture_output=True)
@@ -70,6 +80,16 @@ def setup_cmd():
70
80
  # 2. Install MariaDB and Nginx
71
81
  console.print("\n[bold yellow]Installing MariaDB and Nginx (sudo apt-get install)...[/bold yellow]")
72
82
  subprocess.run(["sudo", "apt-get", "install", "-y", "mariadb-server", "nginx", "curl"], check=True)
83
+
84
+ # Optional mkcert package
85
+ try:
86
+ subprocess.run(["sudo", "apt-get", "install", "-y", "mkcert"], check=False, capture_output=True)
87
+ mkcert_bin = which_binary("mkcert")
88
+ if mkcert_bin:
89
+ subprocess.run([mkcert_bin, "-install"], check=False, capture_output=True)
90
+ except Exception:
91
+ pass
92
+
73
93
  console.print("[bold green]MariaDB and Nginx installed successfully![/bold green]\n")
74
94
 
75
95
  # 3. Install Composer
@@ -71,11 +71,11 @@ def main(
71
71
  console.print(f"\n[bold blue]Upgrading {len(targets)} component(s): {', '.join(targets)}...[/bold blue]\n")
72
72
 
73
73
  for target in targets:
74
- with console.status(f"[bold green]Upgrading {target}...[/bold green]"):
75
- ok, msg = upgrade_rt.upgrade_component(target)
74
+ console.print(f"[bold cyan]Upgrading {target}...[/bold cyan]")
75
+ ok, msg = upgrade_rt.upgrade_component(target)
76
76
  if ok:
77
- console.print(f"[bold green]✓ {msg}[/bold green]")
77
+ console.print(f"[bold green]✓ {msg}[/bold green]\n")
78
78
  else:
79
- console.print(f"[bold red]✗ {msg}[/bold red]")
79
+ console.print(f"[bold red]✗ {msg}[/bold red]\n")
80
80
 
81
81
  console.print("\n[bold green]Upgrade process complete![/bold green]")
@@ -14,6 +14,7 @@ from typing import Optional, Dict, Any, List, Tuple
14
14
  from ndev.common.constants import PHP_DIR, CURRENT_LINK
15
15
  from ndev.common.logger import logger
16
16
  from ndev.common.modules import get_module_manager
17
+ from ndev.common.utils import which_binary
17
18
 
18
19
 
19
20
  def resolve_system_service_name(service: str) -> str:
@@ -52,8 +53,13 @@ def system_service_exists(service: str) -> bool:
52
53
  resolved = resolve_system_service_name(service)
53
54
  if shutil.which("systemctl"):
54
55
  res = subprocess.run(["systemctl", "list-unit-files", f"{resolved}.service"], capture_output=True, text=True)
55
- return resolved in res.stdout
56
- return Path(f"/etc/init.d/{resolved}").exists() or bool(shutil.which(resolved))
56
+ if resolved in res.stdout:
57
+ return True
58
+ return (
59
+ Path(f"/etc/init.d/{resolved}").exists()
60
+ or bool(which_binary(resolved))
61
+ or (resolved == "mariadb" and bool(which_binary("mariadbd") or which_binary("mysqld")))
62
+ )
57
63
 
58
64
 
59
65
  def system_service_status(service: str) -> Dict[str, Any]:
@@ -85,9 +91,10 @@ def execute_system_service_action(service: str, action: str) -> Tuple[bool, str]
85
91
  resolved = resolve_system_service_name(service)
86
92
 
87
93
  # For Nginx reload/restart, validate configuration first
94
+ nginx_bin = which_binary("nginx")
88
95
  if resolved == "nginx" and action in ["reload", "restart"]:
89
- if shutil.which("nginx"):
90
- t_res = subprocess.run(["nginx", "-t"], capture_output=True, text=True)
96
+ if nginx_bin:
97
+ t_res = subprocess.run([nginx_bin, "-t"], capture_output=True, text=True)
91
98
  if t_res.returncode != 0:
92
99
  err = t_res.stderr.strip() or t_res.stdout.strip()
93
100
  return False, f"Nginx configuration test failed:\n{err}"
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  import json
8
8
  import re
9
9
  import shutil
10
+ import platform
10
11
  import subprocess
11
12
  import urllib.request
12
13
  from dataclasses import dataclass
@@ -14,6 +15,7 @@ from pathlib import Path
14
15
  from typing import Optional
15
16
 
16
17
  from ndev.common.constants import NDEV_DIR
18
+ from ndev.common.utils import which_binary
17
19
  from ndev.linux.runtime import mailpit as mailpit_rt, pma as pma_rt
18
20
 
19
21
  USER_AGENT = "ndev/0.1.0"
@@ -80,14 +82,14 @@ def _upgrade_system_packages(packages: list[str]) -> tuple[bool, str]:
80
82
  """Upgrade system packages using zypper or apt."""
81
83
  if shutil.which("zypper"):
82
84
  try:
83
- subprocess.run(["sudo", "zypper", "--non-interactive", "update", "-y"] + packages, check=True)
85
+ subprocess.run(["sudo", "zypper", "--non-interactive", "install", "-y"] + packages, check=True)
84
86
  return True, "Packages upgraded successfully via zypper."
85
87
  except Exception as e:
86
88
  return False, f"zypper upgrade failed: {e}"
87
89
  elif shutil.which("apt") or shutil.which("apt-get"):
88
90
  try:
89
91
  subprocess.run(["sudo", "apt-get", "update"], check=True)
90
- subprocess.run(["sudo", "apt-get", "--only-upgrade", "install", "-y"] + packages, check=True)
92
+ subprocess.run(["sudo", "apt-get", "install", "-y"] + packages, check=True)
91
93
  return True, "Packages upgraded successfully via apt."
92
94
  except Exception as e:
93
95
  return False, f"apt upgrade failed: {e}"
@@ -96,14 +98,15 @@ def _upgrade_system_packages(packages: list[str]) -> tuple[bool, str]:
96
98
 
97
99
  # 1. NGINX
98
100
  def get_nginx_info() -> ComponentInfo:
99
- installed = bool(shutil.which("nginx"))
101
+ nginx_bin = which_binary("nginx")
102
+ installed = bool(nginx_bin)
100
103
  curr_ver = None
101
104
  latest_ver = None
102
105
  err = None
103
106
 
104
107
  if installed:
105
108
  try:
106
- res = subprocess.run(["nginx", "-v"], capture_output=True, text=True, timeout=5)
109
+ res = subprocess.run([nginx_bin, "-v"], capture_output=True, text=True, timeout=5)
107
110
  out = res.stderr or res.stdout
108
111
  m = re.search(r"nginx/(\d+\.\d+\.\d+)", out)
109
112
  if m:
@@ -200,14 +203,17 @@ def upgrade_mailpit() -> tuple[bool, str]:
200
203
 
201
204
  # 3. MARIADB
202
205
  def get_mariadb_info() -> ComponentInfo:
203
- installed = bool(shutil.which("mariadb") or shutil.which("mysql") or shutil.which("mysqld"))
206
+ installed_exe = (
207
+ which_binary("mariadb") or which_binary("mariadbd") or which_binary("mysql") or which_binary("mysqld")
208
+ )
209
+ installed = bool(installed_exe)
204
210
  curr_ver = None
205
211
  latest_ver = None
206
212
  err = None
207
213
 
208
214
  if installed:
209
215
  try:
210
- exe = shutil.which("mariadbd") or shutil.which("mysqld") or shutil.which("mysql")
216
+ exe = installed_exe
211
217
  if exe:
212
218
  res = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=5)
213
219
  m = re.search(r"(\d+\.\d+\.\d+)", res.stdout or res.stderr)
@@ -317,14 +323,15 @@ def upgrade_pma() -> tuple[bool, str]:
317
323
 
318
324
  # 5. MKCERT
319
325
  def get_mkcert_info() -> ComponentInfo:
320
- installed = bool(shutil.which("mkcert"))
326
+ mkcert_bin = which_binary("mkcert")
327
+ installed = bool(mkcert_bin)
321
328
  curr_ver = None
322
329
  latest_ver = None
323
330
  err = None
324
331
 
325
332
  if installed:
326
333
  try:
327
- res = subprocess.run(["mkcert", "-version"], capture_output=True, text=True, timeout=5)
334
+ res = subprocess.run([mkcert_bin, "-version"], capture_output=True, text=True, timeout=5)
328
335
  curr_ver = res.stdout.strip() or res.stderr.strip()
329
336
  except Exception as e:
330
337
  err = str(e)
@@ -357,44 +364,67 @@ def _download_latest_mkcert() -> tuple[bool, str]:
357
364
  target_ver = f"v{target_ver}"
358
365
  dest = Path.home() / ".local" / "bin" / "mkcert"
359
366
  dest.parent.mkdir(parents=True, exist_ok=True)
360
- url = f"https://github.com/FiloSottile/mkcert/releases/download/{target_ver}/mkcert-{target_ver}-linux-amd64"
367
+
368
+ m_arch = platform.machine().lower()
369
+ if m_arch in ["x86_64", "amd64"]:
370
+ arch_suffix = "amd64"
371
+ elif m_arch in ["aarch64", "arm64"]:
372
+ arch_suffix = "arm64"
373
+ elif "arm" in m_arch:
374
+ arch_suffix = "arm"
375
+ else:
376
+ arch_suffix = "amd64"
377
+
378
+ url = f"https://github.com/FiloSottile/mkcert/releases/download/{target_ver}/mkcert-{target_ver}-linux-{arch_suffix}"
361
379
  try:
362
380
  req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
363
381
  with urllib.request.urlopen(req, timeout=120) as resp, open(dest, "wb") as f:
364
382
  f.write(resp.read())
365
383
  dest.chmod(0o755)
366
- subprocess.run([str(dest), "-install"], check=True)
384
+
385
+ # Also link to ~/.ndev/bin/mkcert
386
+ ndev_bin = NDEV_DIR / "bin" / "mkcert"
387
+ try:
388
+ ndev_bin.parent.mkdir(parents=True, exist_ok=True)
389
+ if ndev_bin.exists() or ndev_bin.is_symlink():
390
+ ndev_bin.unlink()
391
+ ndev_bin.symlink_to(dest)
392
+ except Exception:
393
+ pass
394
+
395
+ # Attempt -install if passwordless sudo or root is available; don't block on password prompt
396
+ try:
397
+ can_elevate = (os.geteuid() == 0)
398
+ if not can_elevate and shutil.which("sudo"):
399
+ res = subprocess.run(["sudo", "-n", "true"], capture_output=True)
400
+ can_elevate = (res.returncode == 0)
401
+ if can_elevate:
402
+ subprocess.run([str(dest), "-install"], check=False, capture_output=True)
403
+ except Exception:
404
+ pass
405
+
367
406
  info = get_mkcert_info()
368
- return True, f"mkcert upgraded successfully ({info.current_version})."
407
+ return True, f"mkcert installed/upgraded successfully ({info.current_version or target_ver})."
369
408
  except Exception as e:
370
409
  return False, f"mkcert binary download failed: {e}"
371
410
 
372
411
 
373
412
  def upgrade_mkcert() -> tuple[bool, str]:
374
- if shutil.which("zypper") or shutil.which("apt") or shutil.which("apt-get"):
375
- ok, _msg = _upgrade_system_packages(["mkcert"])
376
- if ok:
377
- try:
378
- subprocess.run(["mkcert", "-install"], check=True)
379
- except Exception:
380
- pass
381
- info = get_mkcert_info()
382
- return True, f"mkcert upgraded successfully ({info.current_version})."
383
-
384
- # Fallback to direct download
413
+ """Download and set up latest standalone mkcert binary (unprivileged, zero sudo required)."""
385
414
  return _download_latest_mkcert()
386
415
 
387
416
 
388
417
  # 6. COMPOSER
389
418
  def get_composer_info() -> ComponentInfo:
390
- installed = bool(shutil.which("composer"))
419
+ composer_bin = which_binary("composer")
420
+ installed = bool(composer_bin)
391
421
  curr_ver = None
392
422
  latest_ver = None
393
423
  err = None
394
424
 
395
425
  if installed:
396
426
  try:
397
- res = subprocess.run(["composer", "--version"], capture_output=True, text=True, timeout=10)
427
+ res = subprocess.run([composer_bin, "--version"], capture_output=True, text=True, timeout=10)
398
428
  m = re.search(r"Composer (?:version )?(\d+\.\d+\.\d+)", res.stdout or res.stderr)
399
429
  if m:
400
430
  curr_ver = m.group(1)
@@ -425,8 +455,9 @@ def get_composer_info() -> ComponentInfo:
425
455
 
426
456
  def upgrade_composer() -> tuple[bool, str]:
427
457
  try:
428
- if shutil.which("composer"):
429
- res = subprocess.run(["composer", "self-update"], capture_output=True, text=True, timeout=60)
458
+ composer_bin = which_binary("composer")
459
+ if composer_bin:
460
+ res = subprocess.run([composer_bin, "self-update"], capture_output=True, text=True, timeout=60)
430
461
  if res.returncode == 0:
431
462
  info = get_composer_info()
432
463
  return True, f"Composer upgraded successfully ({info.current_version})."
@@ -35,6 +35,7 @@ from textual.widgets import (
35
35
  )
36
36
 
37
37
  from ndev.common.constants import NDEV_DIR, PHP_DIR, CURRENT_LINK, LOGS_DIR
38
+ from ndev.common.utils import which_binary
38
39
  from ndev.linux.runtime.fpm import get_fpm_status, start_fpm, stop_fpm, restart_fpm
39
40
  from ndev.linux.runtime.pma import get_pma_status, start_pma, stop_pma, restart_pma, setup_pma
40
41
  from ndev.linux.runtime.mailpit import get_mailpit_status, start_mailpit, stop_mailpit, restart_mailpit, is_installed as is_mailpit_installed, setup_mailpit
@@ -570,7 +571,7 @@ class NdevDashboard(App):
570
571
  results = []
571
572
 
572
573
  # 1. Nginx
573
- ng_installed = bool(shutil.which("nginx"))
574
+ ng_installed = bool(which_binary("nginx"))
574
575
  ng_running = False
575
576
  if ng_installed:
576
577
  if shutil.which("systemctl"):
@@ -591,7 +592,7 @@ class NdevDashboard(App):
591
592
  })
592
593
 
593
594
  # 2. MariaDB / MySQL
594
- db_installed = bool(shutil.which("mariadb") or shutil.which("mysql"))
595
+ db_installed = bool(which_binary("mariadb") or which_binary("mysql") or which_binary("mariadbd") or which_binary("mysqld"))
595
596
  db_running = False
596
597
  if db_installed:
597
598
  if shutil.which("systemctl"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.4
3
+ Version: 0.3.6
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.4"
7
+ version = "0.3.6"
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"
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  import tempfile
11
11
  import shutil
12
12
 
13
- from ndev.common import distro
13
+ from ndev.common import distro, utils
14
14
  from ndev.linux.runtime import nginx_helper
15
15
  from ndev.linux.runtime import services
16
16
  from ndev.linux.runtime import process
@@ -230,6 +230,17 @@ class TestSocketsAndProcess(unittest.TestCase):
230
230
  mock_kill.side_effect = ProcessLookupError("No such process")
231
231
  self.assertFalse(process.is_pid_running(5678))
232
232
 
233
+ def test_which_binary_finds_sbin(self):
234
+ with patch.dict(os.environ, {"PATH": "/usr/bin:/bin"}):
235
+ with patch("shutil.which") as mock_which:
236
+ def fake_which(cmd, path=None):
237
+ if path and "/usr/sbin" in path and cmd == "nginx":
238
+ return "/usr/sbin/nginx"
239
+ return None
240
+ mock_which.side_effect = fake_which
241
+ found = utils.which_binary("nginx")
242
+ self.assertEqual(found, "/usr/sbin/nginx")
243
+
233
244
 
234
245
  class TestServiceManager(unittest.TestCase):
235
246
  def test_resolve_service_name(self):
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