ndev-stack 0.3.10__tar.gz → 0.3.12__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.10 → ndev_stack-0.3.12}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/__init__.py +1 -1
  3. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/vhost.py +18 -5
  4. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/nginx_helper.py +91 -7
  5. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/tui.py +29 -12
  6. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/PKG-INFO +1 -1
  7. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/pyproject.toml +1 -1
  8. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/tests/test_opensuse_compat.py +59 -0
  9. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/README.md +0 -0
  10. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/__main__.py +0 -0
  11. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/cli.py +0 -0
  12. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/__init__.py +0 -0
  13. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/config.py +0 -0
  14. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/constants.py +0 -0
  15. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/distro.py +0 -0
  16. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/github.py +0 -0
  17. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/logger.py +0 -0
  18. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/manifest.py +0 -0
  19. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/__init__.py +0 -0
  20. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/base.py +0 -0
  21. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  22. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  23. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  24. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  25. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  26. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/postgres/module.py +0 -0
  27. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  28. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/builtin/redis/module.py +0 -0
  29. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/modules/manager.py +0 -0
  30. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/common/utils.py +0 -0
  31. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/__init__.py +0 -0
  32. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/manager.py +0 -0
  33. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/packages.py +0 -0
  34. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/chroot/shell.py +0 -0
  35. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/cli.py +0 -0
  36. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/available.py +0 -0
  37. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/clean.py +0 -0
  38. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/ctl.py +0 -0
  39. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/current.py +0 -0
  40. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/db.py +0 -0
  41. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/doctor.py +0 -0
  42. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/grok.py +0 -0
  43. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/install.py +0 -0
  44. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/list.py +0 -0
  45. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/logs.py +0 -0
  46. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/mailpit.py +0 -0
  47. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/modules.py +0 -0
  48. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/mongodb.py +0 -0
  49. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/postgres.py +0 -0
  50. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/reload.py +0 -0
  51. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/restart.py +0 -0
  52. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/setup.py +0 -0
  53. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/start.py +0 -0
  54. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/status.py +0 -0
  55. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/stop.py +0 -0
  56. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/uninstall.py +0 -0
  57. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/update.py +0 -0
  58. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/upgrade.py +0 -0
  59. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/commands/use.py +0 -0
  60. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/builder.py +0 -0
  61. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/downloader.py +0 -0
  62. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/extensions.py +0 -0
  63. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/installer.py +0 -0
  64. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/resolver.py +0 -0
  65. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/php/templates.py +0 -0
  66. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/fpm.py +0 -0
  67. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/mailpit.py +0 -0
  68. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/pma.py +0 -0
  69. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/process.py +0 -0
  70. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/services.py +0 -0
  71. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/sockets.py +0 -0
  72. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/linux/runtime/upgrade.py +0 -0
  73. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/main.py +0 -0
  74. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/tui.py +0 -0
  75. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/__init__.py +0 -0
  76. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/bin/shim.exe +0 -0
  77. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/cli.py +0 -0
  78. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/commands/__init__.py +0 -0
  79. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/__init__.py +0 -0
  80. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/db.py +0 -0
  81. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/elevate.py +0 -0
  82. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/ext.py +0 -0
  83. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/fcgi.py +0 -0
  84. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/grok.py +0 -0
  85. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/logs.py +0 -0
  86. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/mailpit.py +0 -0
  87. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/mkcert.py +0 -0
  88. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/paths.py +0 -0
  89. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/php.py +0 -0
  90. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/pma.py +0 -0
  91. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/redis_core.py +0 -0
  92. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/services.py +0 -0
  93. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/setup.py +0 -0
  94. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/shim.py +0 -0
  95. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/upgrade.py +0 -0
  96. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/core/vhost.py +0 -0
  97. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/templates/vhost.conf.tmpl +0 -0
  98. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  99. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev/win/tui.py +0 -0
  100. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/SOURCES.txt +0 -0
  101. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/dependency_links.txt +0 -0
  102. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/entry_points.txt +0 -0
  103. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/requires.txt +0 -0
  104. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/ndev_stack.egg-info/top_level.txt +0 -0
  105. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/setup.cfg +0 -0
  106. {ndev_stack-0.3.10 → ndev_stack-0.3.12}/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.10
3
+ Version: 0.3.12
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.10"
4
+ __version__ = "0.3.12"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -319,6 +319,8 @@ def vhost_cmd(
319
319
  get_nginx_vhost_dirs,
320
320
  generate_vhost_config,
321
321
  ensure_nginx_sites_enabled_included,
322
+ install_nginx_ssl_cert,
323
+ ensure_docroot_accessible,
322
324
  add_to_hosts_privileged,
323
325
  reload_nginx,
324
326
  )
@@ -338,14 +340,25 @@ def vhost_cmd(
338
340
  if enabled_dir:
339
341
  ensure_nginx_sites_enabled_included()
340
342
 
343
+ # Install SSL certificates into /etc/nginx/ssl for systemd / SELinux compatibility
344
+ final_cert_path = cert_path
345
+ final_key_path = key_path
346
+ if ssl and cert_path and key_path:
347
+ try:
348
+ final_cert_path, final_key_path = install_nginx_ssl_cert(domain, cert_path, key_path)
349
+ except Exception as e:
350
+ logger.warning(f"Could not install SSL certificate to /etc/nginx/ssl: {e}")
351
+
352
+ ensure_docroot_accessible(root_path)
353
+
341
354
  conf_file = vhost_dir / f"{domain}.conf"
342
355
  config_template = generate_vhost_config(
343
356
  domain=domain,
344
- root=root,
357
+ root=str(root_path),
345
358
  selected_sock=selected_sock,
346
359
  ssl=bool(ssl),
347
- cert_path=cert_path,
348
- key_path=key_path,
360
+ cert_path=final_cert_path,
361
+ key_path=final_key_path,
349
362
  )
350
363
 
351
364
  enabled_link = None
@@ -382,8 +395,8 @@ def vhost_cmd(
382
395
  if enabled_link:
383
396
  console.print(f"Symlink : {enabled_link}")
384
397
  if ssl:
385
- console.print(f"SSL Cert : {cert_path}")
386
- console.print(f"SSL Key : {key_path}")
398
+ console.print(f"SSL Cert : {final_cert_path}")
399
+ console.print(f"SSL Key : {final_key_path}")
387
400
  console.print(f"\nOpen: https://{domain}")
388
401
  else:
389
402
  console.print(f"\nOpen: http://{domain}")
@@ -426,13 +426,24 @@ server {{
426
426
 
427
427
  def reload_nginx(password: Optional[str] = None) -> bool:
428
428
  """
429
- Reload Nginx configuration safely using available init system or nginx CLI.
429
+ Reload Nginx configuration if active, or start Nginx if stopped.
430
430
  """
431
+ if os.geteuid() != 0 and password:
432
+ validate_sudo_password(password)
433
+
431
434
  cmd = None
432
435
  if shutil.which("systemctl"):
433
- cmd = ["systemctl", "reload", "nginx"]
436
+ res = subprocess.run(["systemctl", "is-active", "--quiet", "nginx"])
437
+ if res.returncode == 0:
438
+ cmd = ["systemctl", "reload", "nginx"]
439
+ else:
440
+ cmd = ["systemctl", "start", "nginx"]
434
441
  elif shutil.which("service"):
435
- cmd = ["service", "nginx", "reload"]
442
+ res = subprocess.run(["service", "nginx", "status"], capture_output=True)
443
+ if res.returncode == 0:
444
+ cmd = ["service", "nginx", "reload"]
445
+ else:
446
+ cmd = ["service", "nginx", "start"]
436
447
  else:
437
448
  nginx_bin = which_binary("nginx") or shutil.which("nginx")
438
449
  if nginx_bin:
@@ -442,18 +453,88 @@ def reload_nginx(password: Optional[str] = None) -> bool:
442
453
  return False
443
454
 
444
455
  if os.geteuid() != 0:
445
- if password:
446
- validate_sudo_password(password)
447
456
  cmd = ["sudo", "-n"] + cmd
448
457
 
449
458
  try:
450
459
  res = subprocess.run(cmd, capture_output=True, timeout=10)
451
460
  return res.returncode == 0
452
461
  except Exception as e:
453
- logger.warning(f"Could not reload Nginx: {e}")
462
+ logger.warning(f"Could not reload/start Nginx: {e}")
454
463
  return False
455
464
 
456
465
 
466
+ def install_nginx_ssl_cert(
467
+ domain: str,
468
+ src_cert: Path | str,
469
+ src_key: Path | str,
470
+ password: Optional[str] = None
471
+ ) -> Tuple[Path, Path]:
472
+ """
473
+ Install SSL certificate and key into /etc/nginx/ssl/{domain}.* with proper permissions and SELinux label.
474
+ Fixes 'BIO_new_file() failed (SSL: error:8000000d:system library:BIO_new_file:Permission denied)'
475
+ caused by systemd ProtectHome and SELinux restricting access to ~/.ndev/certs.
476
+ Returns (installed_cert_path, installed_key_path).
477
+ """
478
+ ssl_dir = Path("/etc/nginx/ssl")
479
+ dest_cert = ssl_dir / f"{domain}.crt"
480
+ dest_key = ssl_dir / f"{domain}.key"
481
+
482
+ cert_content = Path(src_cert).read_text(encoding="utf-8")
483
+ key_content = Path(src_key).read_text(encoding="utf-8")
484
+
485
+ write_privileged_file(dest_cert, cert_content, password=password)
486
+ write_privileged_file(dest_key, key_content, password=password)
487
+
488
+ if os.geteuid() == 0:
489
+ try:
490
+ os.chmod(str(dest_cert), 0o644)
491
+ os.chmod(str(dest_key), 0o644)
492
+ except Exception:
493
+ pass
494
+ else:
495
+ try:
496
+ os.chmod(str(dest_cert), 0o644)
497
+ os.chmod(str(dest_key), 0o644)
498
+ except (PermissionError, OSError):
499
+ subprocess.run(["sudo", "-n", "chmod", "644", str(dest_cert), str(dest_key)], check=False)
500
+
501
+ if shutil.which("restorecon"):
502
+ cmd = ["restorecon", "-Rv", str(ssl_dir)]
503
+ if os.geteuid() != 0:
504
+ cmd = ["sudo", "-n"] + cmd
505
+ try:
506
+ subprocess.run(cmd, capture_output=True)
507
+ except Exception:
508
+ pass
509
+
510
+ return dest_cert, dest_key
511
+
512
+
513
+ def ensure_docroot_accessible(root_path: Path) -> None:
514
+ """
515
+ Ensure the parent directories up to root_path have execute permission (+x)
516
+ so the nginx worker process (e.g. user nginx) can traverse into document root.
517
+ """
518
+ home = Path.home()
519
+ sudo_user = os.environ.get("SUDO_USER")
520
+ if sudo_user:
521
+ try:
522
+ import pwd
523
+ home = Path(pwd.getpwnam(sudo_user).pw_dir)
524
+ except Exception:
525
+ pass
526
+
527
+ try:
528
+ resolved_root = root_path.resolve()
529
+ resolved_home = home.resolve()
530
+ if resolved_root == resolved_home or resolved_root.is_relative_to(resolved_home):
531
+ st = home.stat()
532
+ if not (st.st_mode & 0o001):
533
+ os.chmod(str(home), st.st_mode | 0o001)
534
+ except Exception:
535
+ pass
536
+
537
+
457
538
  def list_vhosts() -> List[Dict[str, Any]]:
458
539
  """List all configured virtual hosts across all searched Nginx config directories."""
459
540
  vhosts = []
@@ -513,8 +594,11 @@ def remove_vhost(domain: str, password: Optional[str] = None) -> bool:
513
594
  except Exception as e:
514
595
  logger.warning(f"Could not remove {conf_file}: {e}")
515
596
 
516
- # Remove optional certificates
597
+ # Remove system and optional certificates
517
598
  for cert_ext in [".crt", ".key"]:
599
+ sys_cert = Path(f"/etc/nginx/ssl/{clean_domain}{cert_ext}")
600
+ unlink_privileged(sys_cert, password=password)
601
+
518
602
  cfile = NDEV_DIR / "certs" / f"{clean_domain}{cert_ext}"
519
603
  if cfile.exists():
520
604
  try:
@@ -361,7 +361,7 @@ class CreateVhostModal(ModalScreen[Optional[dict]]):
361
361
  if not root and domain:
362
362
  root = str(Path.home() / "Sites" / domain)
363
363
  php_val = self.query_one("#select-vhost-php", Select).value
364
- php_ver = str(php_val) if (php_val is not None and php_val != Select.BLANK) else self.default_php
364
+ php_ver = str(php_val) if (php_val is not None and php_val != Select.BLANK and php_val != getattr(Select, "NULL", None)) else self.default_php
365
365
  ssl = self.query_one("#chk-vhost-ssl", Checkbox).value
366
366
 
367
367
  if not domain:
@@ -474,7 +474,7 @@ class InstallPhpModal(ModalScreen[Optional[dict]]):
474
474
  if event.button.id == "btn-modal-install":
475
475
  custom = self.query_one("#input-custom-ver", Input).value.strip()
476
476
  sel_val = self.query_one("#select-php-ver", Select).value
477
- selected = str(sel_val) if (sel_val is not None and sel_val != Select.BLANK) else "8.4.25"
477
+ selected = str(sel_val) if (sel_val is not None and sel_val != Select.BLANK and sel_val != getattr(Select, "NULL", None)) else "8.4.25"
478
478
  version = custom if custom else selected
479
479
 
480
480
  self.dismiss({
@@ -1203,6 +1203,8 @@ class NdevDashboard(App):
1203
1203
  get_nginx_vhost_dirs,
1204
1204
  generate_vhost_config,
1205
1205
  ensure_nginx_sites_enabled_included,
1206
+ install_nginx_ssl_cert,
1207
+ ensure_docroot_accessible,
1206
1208
  write_privileged_file,
1207
1209
  symlink_privileged,
1208
1210
  add_to_hosts_privileged,
@@ -1220,6 +1222,18 @@ class NdevDashboard(App):
1220
1222
  if ssl:
1221
1223
  cert_path, key_path = generate_local_cert(domain, cert_dir)
1222
1224
 
1225
+ final_cert_path = cert_path
1226
+ final_key_path = key_path
1227
+ if ssl and cert_path and key_path:
1228
+ try:
1229
+ final_cert_path, final_key_path = install_nginx_ssl_cert(
1230
+ domain, cert_path, key_path, password=password
1231
+ )
1232
+ except Exception as e:
1233
+ logger.warning(f"Could not install SSL certificate into /etc/nginx/ssl: {e}")
1234
+
1235
+ ensure_docroot_accessible(Path(root))
1236
+
1223
1237
  mm = php_ver.replace(".", "")[:2] if "." in php_ver else php_ver
1224
1238
  selected_sock = NDEV_DIR / "run" / f"php{mm}.sock"
1225
1239
 
@@ -1228,8 +1242,8 @@ class NdevDashboard(App):
1228
1242
  root=root,
1229
1243
  selected_sock=selected_sock,
1230
1244
  ssl=bool(ssl),
1231
- cert_path=cert_path,
1232
- key_path=key_path,
1245
+ cert_path=final_cert_path,
1246
+ key_path=final_key_path,
1233
1247
  )
1234
1248
  write_privileged_file(conf_file, tpl, password=password)
1235
1249
  if enabled_dir and enabled_dir != vhost_dir:
@@ -1430,19 +1444,22 @@ class NdevDashboard(App):
1430
1444
  self.log_message(f"[bold red]✗ Failed to install {key}: {e}[/bold red]")
1431
1445
  self.notify(f"Installation failed: {e}", severity="error")
1432
1446
 
1433
- @work(exclusive=True)
1434
- async def on_select_changed(self, event: Select.Changed) -> None:
1447
+ def on_select_changed(self, event: Select.Changed) -> None:
1435
1448
  """Handle PHP version switcher or log selector dropdown changes."""
1436
1449
  if self._updating_selects:
1437
1450
  return
1438
1451
 
1439
- if event.select.id == "select-php-version" and event.value != Select.BLANK:
1440
- target_version = str(event.value)
1452
+ val = event.value
1453
+ if val in (Select.BLANK, getattr(Select, "NULL", None), None) or not isinstance(val, str):
1454
+ return
1455
+
1456
+ if event.select.id == "select-php-version":
1457
+ target_version = val.strip()
1441
1458
  curr = CURRENT_LINK.resolve().name if (CURRENT_LINK.exists() or CURRENT_LINK.is_symlink()) else None
1442
- if target_version != curr:
1443
- await self._switch_php_version(target_version)
1459
+ if target_version and target_version != curr and (PHP_DIR / target_version).exists():
1460
+ self._switch_php_version(target_version)
1444
1461
 
1445
- elif event.select.id == "select-log-file" and event.value != Select.BLANK:
1462
+ elif event.select.id == "select-log-file":
1446
1463
  self._handle_tail_log()
1447
1464
 
1448
1465
  def _handle_tail_log(self) -> None:
@@ -1450,7 +1467,7 @@ class NdevDashboard(App):
1450
1467
  self.query_one(TabbedContent).active = "tab-logs"
1451
1468
  log_select = self.query_one("#select-log-file", Select)
1452
1469
  log_name = log_select.value
1453
- if not log_name or log_name == Select.BLANK:
1470
+ if not log_name or log_name in (Select.BLANK, getattr(Select, "NULL", None)):
1454
1471
  if self._available_logs:
1455
1472
  first_key = sorted(self._available_logs.keys())[0]
1456
1473
  self._updating_selects = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.10
3
+ Version: 0.3.12
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.10"
7
+ version = "0.3.12"
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"
@@ -255,6 +255,27 @@ class TestNginxHelper(unittest.TestCase):
255
255
  mock_run.return_value = unittest.mock.MagicMock(returncode=1)
256
256
  self.assertTrue(nginx_helper.check_sudo_needs_password())
257
257
 
258
+ def test_install_nginx_ssl_cert(self):
259
+ src_cert = self.test_dir / "src.crt"
260
+ src_key = self.test_dir / "src.key"
261
+ src_cert.write_text("CERT_CONTENT")
262
+ src_key.write_text("KEY_CONTENT")
263
+
264
+ fake_ssl_dir = self.test_dir / "nginx_ssl"
265
+ with patch("ndev.linux.runtime.nginx_helper.Path") as mock_path:
266
+ def fake_p(p):
267
+ if str(p) == "/etc/nginx/ssl":
268
+ return fake_ssl_dir
269
+ return Path(p)
270
+ mock_path.side_effect = fake_p
271
+
272
+ with patch("os.geteuid", return_value=0):
273
+ dest_cert, dest_key = nginx_helper.install_nginx_ssl_cert("sample.local", src_cert, src_key)
274
+ self.assertTrue(dest_cert.exists())
275
+ self.assertTrue(dest_key.exists())
276
+ self.assertEqual(dest_cert.read_text(), "CERT_CONTENT")
277
+ self.assertEqual(dest_key.read_text(), "KEY_CONTENT")
278
+
258
279
  def test_fastcgi_snippet_opensuse(self):
259
280
  with patch.object(Path, "exists", autospec=True) as mock_exists:
260
281
  def side_effect_exists(p):
@@ -443,5 +464,43 @@ class TestPhpDevelPackagesAndBuilder(unittest.TestCase):
443
464
  self.assertEqual(resolve_installed_version("7.4"), "7.4")
444
465
 
445
466
 
467
+ class TestTuiSelectEventHandling(unittest.TestCase):
468
+ def test_on_select_changed_handles_null_and_blank(self):
469
+ from ndev.linux.tui import NdevDashboard
470
+ from textual.widgets import Select
471
+
472
+ app = NdevDashboard()
473
+ app._switch_php_version = MagicMock()
474
+
475
+ # Mock Select.Changed with Select.NULL
476
+ mock_event_null = MagicMock()
477
+ mock_event_null.select.id = "select-php-version"
478
+ mock_event_null.value = getattr(Select, "NULL", None)
479
+
480
+ app.on_select_changed(mock_event_null)
481
+ app._switch_php_version.assert_not_called()
482
+
483
+ # Mock Select.Changed with Select.BLANK
484
+ mock_event_blank = MagicMock()
485
+ mock_event_blank.select.id = "select-php-version"
486
+ mock_event_blank.value = Select.BLANK
487
+
488
+ app.on_select_changed(mock_event_blank)
489
+ app._switch_php_version.assert_not_called()
490
+
491
+ # Mock Select.Changed with valid version
492
+ mock_event_valid = MagicMock()
493
+ mock_event_valid.select.id = "select-php-version"
494
+ mock_event_valid.value = "8.4.27"
495
+
496
+ with patch("ndev.linux.tui.PHP_DIR") as mock_php_dir:
497
+ mock_php_dir.__truediv__.return_value.exists.return_value = True
498
+ with patch("ndev.linux.tui.CURRENT_LINK") as mock_link:
499
+ mock_link.resolve.return_value.name = "8.2.35"
500
+ mock_link.exists.return_value = True
501
+ app.on_select_changed(mock_event_valid)
502
+ app._switch_php_version.assert_called_once_with("8.4.27")
503
+
504
+
446
505
  if __name__ == "__main__":
447
506
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes