ndev-stack 0.1.0__py3-none-any.whl

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 (79) hide show
  1. ndev/__init__.py +8 -0
  2. ndev/__main__.py +4 -0
  3. ndev/cli.py +24 -0
  4. ndev/common/__init__.py +3 -0
  5. ndev/common/config.py +114 -0
  6. ndev/common/constants.py +51 -0
  7. ndev/common/github.py +13 -0
  8. ndev/common/logger.py +11 -0
  9. ndev/common/manifest.py +41 -0
  10. ndev/common/utils.py +96 -0
  11. ndev/linux/__init__.py +1 -0
  12. ndev/linux/chroot/manager.py +63 -0
  13. ndev/linux/chroot/packages.py +91 -0
  14. ndev/linux/chroot/shell.py +9 -0
  15. ndev/linux/cli.py +235 -0
  16. ndev/linux/commands/available.py +38 -0
  17. ndev/linux/commands/clean.py +25 -0
  18. ndev/linux/commands/ctl.py +192 -0
  19. ndev/linux/commands/current.py +11 -0
  20. ndev/linux/commands/db.py +319 -0
  21. ndev/linux/commands/doctor.py +56 -0
  22. ndev/linux/commands/grok.py +75 -0
  23. ndev/linux/commands/install.py +39 -0
  24. ndev/linux/commands/list.py +47 -0
  25. ndev/linux/commands/logs.py +36 -0
  26. ndev/linux/commands/mailpit.py +82 -0
  27. ndev/linux/commands/reload.py +26 -0
  28. ndev/linux/commands/restart.py +34 -0
  29. ndev/linux/commands/setup.py +113 -0
  30. ndev/linux/commands/start.py +34 -0
  31. ndev/linux/commands/status.py +81 -0
  32. ndev/linux/commands/stop.py +34 -0
  33. ndev/linux/commands/uninstall.py +69 -0
  34. ndev/linux/commands/update.py +57 -0
  35. ndev/linux/commands/upgrade.py +81 -0
  36. ndev/linux/commands/use.py +108 -0
  37. ndev/linux/commands/vhost.py +350 -0
  38. ndev/linux/php/builder.py +183 -0
  39. ndev/linux/php/downloader.py +58 -0
  40. ndev/linux/php/extensions.py +146 -0
  41. ndev/linux/php/installer.py +42 -0
  42. ndev/linux/php/resolver.py +59 -0
  43. ndev/linux/php/templates.py +128 -0
  44. ndev/linux/runtime/fpm.py +117 -0
  45. ndev/linux/runtime/mailpit.py +244 -0
  46. ndev/linux/runtime/pma.py +223 -0
  47. ndev/linux/runtime/process.py +37 -0
  48. ndev/linux/runtime/sockets.py +16 -0
  49. ndev/linux/runtime/upgrade.py +431 -0
  50. ndev/linux/tui.py +1423 -0
  51. ndev/main.py +52 -0
  52. ndev/tui.py +23 -0
  53. ndev/win/__init__.py +1 -0
  54. ndev/win/cli.py +1898 -0
  55. ndev/win/commands/__init__.py +0 -0
  56. ndev/win/core/__init__.py +0 -0
  57. ndev/win/core/db.py +265 -0
  58. ndev/win/core/elevate.py +94 -0
  59. ndev/win/core/ext.py +241 -0
  60. ndev/win/core/fcgi.py +216 -0
  61. ndev/win/core/grok.py +55 -0
  62. ndev/win/core/logs.py +66 -0
  63. ndev/win/core/mailpit.py +236 -0
  64. ndev/win/core/mkcert.py +65 -0
  65. ndev/win/core/paths.py +85 -0
  66. ndev/win/core/php.py +533 -0
  67. ndev/win/core/pma.py +190 -0
  68. ndev/win/core/services.py +349 -0
  69. ndev/win/core/setup.py +361 -0
  70. ndev/win/core/upgrade.py +513 -0
  71. ndev/win/core/vhost.py +289 -0
  72. ndev/win/templates/vhost.conf.tmpl +33 -0
  73. ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
  74. ndev/win/tui.py +1313 -0
  75. ndev_stack-0.1.0.dist-info/METADATA +553 -0
  76. ndev_stack-0.1.0.dist-info/RECORD +79 -0
  77. ndev_stack-0.1.0.dist-info/WHEEL +5 -0
  78. ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
  79. ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
ndev/linux/cli.py ADDED
@@ -0,0 +1,235 @@
1
+ import typer
2
+ from ndev.common.logger import logger
3
+ from ndev.common.config import init_layout
4
+
5
+ app = typer.Typer(
6
+ help="ndev: Compile, install, and manage isolated PHP-FPM versions on Debian.",
7
+ no_args_is_help=True
8
+ )
9
+
10
+ ext_app = typer.Typer(
11
+ help="Manage PHP extensions for installed versions.",
12
+ no_args_is_help=True
13
+ )
14
+ app.add_typer(ext_app, name="ext")
15
+
16
+ # Register commands
17
+ from ndev.linux.commands.install import install_cmd
18
+ app.command("install")(install_cmd)
19
+
20
+ from ndev.linux.commands.uninstall import uninstall_cmd
21
+ app.command("uninstall")(uninstall_cmd)
22
+
23
+ from ndev.linux.commands.start import start_cmd
24
+ app.command("start")(start_cmd)
25
+
26
+ from ndev.linux.commands.stop import stop_cmd
27
+ app.command("stop")(stop_cmd)
28
+
29
+ from ndev.linux.commands.restart import restart_cmd
30
+ app.command("restart")(restart_cmd)
31
+
32
+ from ndev.linux.commands.reload import reload_cmd
33
+ app.command("reload")(reload_cmd)
34
+
35
+ from ndev.linux.commands.status import status_cmd
36
+ app.command("status")(status_cmd)
37
+
38
+ from ndev.linux.commands.list import list_cmd
39
+ app.command("list")(list_cmd)
40
+
41
+ from ndev.linux.commands.current import current_cmd
42
+ app.command("current")(current_cmd)
43
+
44
+ from ndev.linux.commands.use import use_cmd
45
+ app.command("use")(use_cmd)
46
+
47
+ from ndev.linux.commands.available import available_cmd
48
+ app.command("available")(available_cmd)
49
+
50
+ from ndev.linux.commands.doctor import doctor_cmd
51
+ app.command("doctor")(doctor_cmd)
52
+
53
+ from ndev.linux.commands.update import update_cmd
54
+ app.command("update")(update_cmd)
55
+
56
+ from ndev.linux.commands.clean import clean_cmd
57
+ app.command("clean")(clean_cmd)
58
+
59
+ from ndev.linux.commands.logs import logs_cmd
60
+ app.command("logs")(logs_cmd)
61
+
62
+ from ndev.linux.commands.grok import grok_cmd
63
+ app.command("grok")(grok_cmd)
64
+
65
+ from ndev.linux.commands.vhost import vhost_cmd
66
+ app.command("vhost")(vhost_cmd)
67
+
68
+ from ndev.linux.commands.ctl import ctl_cmd
69
+ app.command("ctl")(ctl_cmd)
70
+
71
+ from ndev.linux.commands.setup import setup_cmd
72
+ app.command("setup")(setup_cmd)
73
+
74
+ from ndev.linux.commands.upgrade import app as upgrade_app
75
+ app.add_typer(upgrade_app, name="upgrade")
76
+
77
+ from ndev.linux.commands.db import db_app
78
+ app.add_typer(db_app, name="db")
79
+
80
+ from ndev.linux.commands.mailpit import mailpit_app
81
+ app.add_typer(mailpit_app, name="mailpit")
82
+
83
+ @app.command("ui")
84
+ def ui_cmd():
85
+ """Launch the interactive Textual TUI dashboard."""
86
+ from ndev.tui import run_dashboard
87
+ run_dashboard()
88
+
89
+ @app.command("tui", hidden=True)
90
+ def tui_cmd():
91
+ """Alias for ui."""
92
+ from ndev.tui import run_dashboard
93
+ run_dashboard()
94
+
95
+ @app.command("dashboard", hidden=True)
96
+ def dashboard_cmd():
97
+ """Alias for ui."""
98
+ from ndev.tui import run_dashboard
99
+ run_dashboard()
100
+
101
+ @app.command("shell")
102
+ def shell():
103
+ """Enter the interactive bubblewrap build environment shell."""
104
+ from ndev.linux.chroot.shell import enter_sandbox_shell
105
+ enter_sandbox_shell()
106
+
107
+ # Extension command implementations
108
+ @ext_app.command("list")
109
+ def ext_list(version: str = typer.Argument(None, help="PHP version (e.g. 8.4.12)")):
110
+ """List loaded extensions for a PHP version."""
111
+ from ndev.linux.php.extensions import list_extensions
112
+ if not version:
113
+ from ndev.common.utils import get_version_or_prompt
114
+ version = get_version_or_prompt(version, "PHP version")
115
+ if not version:
116
+ logger.error("PHP version is required.")
117
+ raise typer.Exit(code=1)
118
+ try:
119
+ exts = list_extensions(version)
120
+ for ext in exts:
121
+ print(ext)
122
+ except Exception as e:
123
+ logger.error(f"Error listing extensions: {e}")
124
+ raise typer.Exit(code=1)
125
+
126
+ @ext_app.command("install")
127
+ def ext_install(
128
+ ext_name: str = typer.Argument(None, help="Extension name (e.g. redis)"),
129
+ version: str = typer.Argument(None, help="PHP version (e.g. 8.4.12)"),
130
+ show_logs: bool = typer.Option(
131
+ False,
132
+ "--show-logs",
133
+ "-s",
134
+ help="Show verbose compilation and installation logs"
135
+ )
136
+ ):
137
+ """Install and enable a PECL extension."""
138
+ from ndev.linux.php.extensions import install_extension
139
+ if not ext_name:
140
+ ext_name = typer.prompt("Extension name (e.g. redis)").strip()
141
+ if not ext_name:
142
+ logger.error("Extension name is required.")
143
+ raise typer.Exit(code=1)
144
+ if not version:
145
+ from ndev.common.utils import get_version_or_prompt
146
+ version = get_version_or_prompt(version, "PHP version")
147
+ if not version:
148
+ logger.error("PHP version is required.")
149
+ raise typer.Exit(code=1)
150
+ try:
151
+ install_extension(version, ext_name, show_logs=show_logs)
152
+ except Exception as e:
153
+ logger.error(f"Error installing extension: {e}")
154
+ raise typer.Exit(code=1)
155
+
156
+ @ext_app.command("uninstall")
157
+ def ext_uninstall(
158
+ ext_name: str = typer.Argument(None, help="Extension name (e.g. redis)"),
159
+ version: str = typer.Argument(None, help="PHP version (e.g. 8.4.12)")
160
+ ):
161
+ """Disable/uninstall an extension."""
162
+ from ndev.linux.php.extensions import disable_extension
163
+ if not ext_name:
164
+ ext_name = typer.prompt("Extension name (e.g. redis)").strip()
165
+ if not ext_name:
166
+ logger.error("Extension name is required.")
167
+ raise typer.Exit(code=1)
168
+ if not version:
169
+ from ndev.common.utils import get_version_or_prompt
170
+ version = get_version_or_prompt(version, "PHP version")
171
+ if not version:
172
+ logger.error("PHP version is required.")
173
+ raise typer.Exit(code=1)
174
+ try:
175
+ disable_extension(version, ext_name)
176
+ except Exception as e:
177
+ logger.error(f"Error uninstalling extension: {e}")
178
+ raise typer.Exit(code=1)
179
+
180
+ @ext_app.command("enable")
181
+ def ext_enable(
182
+ ext_name: str = typer.Argument(None, help="Extension name (e.g. redis)"),
183
+ version: str = typer.Argument(None, help="PHP version (e.g. 8.4.12)")
184
+ ):
185
+ """Enable an installed extension."""
186
+ from ndev.linux.php.extensions import enable_extension
187
+ if not ext_name:
188
+ ext_name = typer.prompt("Extension name (e.g. redis)").strip()
189
+ if not ext_name:
190
+ logger.error("Extension name is required.")
191
+ raise typer.Exit(code=1)
192
+ if not version:
193
+ from ndev.common.utils import get_version_or_prompt
194
+ version = get_version_or_prompt(version, "PHP version")
195
+ if not version:
196
+ logger.error("PHP version is required.")
197
+ raise typer.Exit(code=1)
198
+ try:
199
+ enable_extension(version, ext_name)
200
+ except Exception as e:
201
+ logger.error(f"Error enabling extension: {e}")
202
+ raise typer.Exit(code=1)
203
+
204
+ @ext_app.command("disable")
205
+ def ext_disable(
206
+ ext_name: str = typer.Argument(None, help="Extension name (e.g. redis)"),
207
+ version: str = typer.Argument(None, help="PHP version (e.g. 8.4.12)")
208
+ ):
209
+ """Disable an extension."""
210
+ from ndev.linux.php.extensions import disable_extension
211
+ if not ext_name:
212
+ ext_name = typer.prompt("Extension name (e.g. redis)").strip()
213
+ if not ext_name:
214
+ logger.error("Extension name is required.")
215
+ raise typer.Exit(code=1)
216
+ if not version:
217
+ from ndev.common.utils import get_version_or_prompt
218
+ version = get_version_or_prompt(version, "PHP version")
219
+ if not version:
220
+ logger.error("PHP version is required.")
221
+ raise typer.Exit(code=1)
222
+ try:
223
+ disable_extension(version, ext_name)
224
+ except Exception as e:
225
+ logger.error(f"Error disabling extension: {e}")
226
+ raise typer.Exit(code=1)
227
+
228
+
229
+ @app.callback()
230
+ def main():
231
+ # Initialize the folder structure in ~/.ndev
232
+ init_layout()
233
+
234
+ if __name__ == "__main__":
235
+ app()
@@ -0,0 +1,38 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from ndev.common.github import fetch_releases
5
+ from ndev.common.logger import logger
6
+
7
+ console = Console()
8
+
9
+ def available_cmd():
10
+ """List all available PHP versions from php.net."""
11
+ logger.info("Fetching available PHP versions from php.net...")
12
+
13
+ releases_8 = fetch_releases(8)
14
+ releases_7 = fetch_releases(7)
15
+
16
+ all_releases = {**releases_8, **releases_7}
17
+
18
+ if not all_releases:
19
+ logger.error("Could not fetch available releases. Check your network connection.")
20
+ raise typer.Exit(code=1)
21
+
22
+ from packaging.version import parse as parse_version
23
+ sorted_versions = sorted(all_releases.keys(), key=parse_version, reverse=True)
24
+
25
+ table = Table(title="Available PHP Releases")
26
+ table.add_column("Version", style="bold cyan")
27
+ table.add_column("Release Date")
28
+ table.add_column("Security Release")
29
+
30
+ for v in sorted_versions:
31
+ data = all_releases[v]
32
+ date = data.get("date", "N/A")
33
+ tags = data.get("tags", [])
34
+ is_security = "Yes" if "security" in tags else "No"
35
+
36
+ table.add_row(v, date, is_security)
37
+
38
+ console.print(table)
@@ -0,0 +1,25 @@
1
+ import shutil
2
+ import typer
3
+ from ndev.common.constants import BUILDS_DIR, DOWNLOADS_DIR
4
+ from ndev.common.logger import logger
5
+
6
+ def clean_cmd(
7
+ builds: bool = typer.Option(True, "--builds/--no-builds", help="Clean extracted build source folders"),
8
+ downloads: bool = typer.Option(False, "--downloads", help="Clean cached download tarballs")
9
+ ):
10
+ """Clean up build files and optionally downloaded archives to free disk space."""
11
+ try:
12
+ if builds and BUILDS_DIR.exists():
13
+ logger.info(f"Cleaning build directory: {BUILDS_DIR}")
14
+ shutil.rmtree(BUILDS_DIR)
15
+ BUILDS_DIR.mkdir()
16
+
17
+ if downloads and DOWNLOADS_DIR.exists():
18
+ logger.info(f"Cleaning downloads cache: {DOWNLOADS_DIR}")
19
+ shutil.rmtree(DOWNLOADS_DIR)
20
+ DOWNLOADS_DIR.mkdir()
21
+
22
+ logger.info("Cleanup completed successfully.")
23
+ except Exception as e:
24
+ logger.error(f"Cleanup failed: {e}")
25
+ raise typer.Exit(code=1)
@@ -0,0 +1,192 @@
1
+ import os
2
+ import sys
3
+ import re
4
+ import subprocess
5
+ import shutil
6
+ import typer
7
+ from pathlib import Path
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from ndev.common.logger import logger
11
+
12
+ console = Console()
13
+
14
+ def get_user_ndev_dir() -> Path:
15
+ sudo_user = os.environ.get("SUDO_USER")
16
+ if sudo_user:
17
+ try:
18
+ import pwd
19
+ return Path(pwd.getpwnam(sudo_user).pw_dir) / ".ndev"
20
+ except Exception:
21
+ pass
22
+ return Path(os.path.expanduser("~/.ndev"))
23
+
24
+ def service_exists(service: str) -> bool:
25
+ if service == "pma":
26
+ return True
27
+ if service.startswith("ndev-"):
28
+ version = service[5:]
29
+ return (get_user_ndev_dir() / "php" / version).exists()
30
+
31
+ if shutil.which("systemctl"):
32
+ res = subprocess.run(["systemctl", "list-unit-files", f"{service}.service"], capture_output=True, text=True)
33
+ return service in res.stdout
34
+ else:
35
+ return Path(f"/etc/init.d/{service}").exists()
36
+
37
+ def service_status(service: str) -> str:
38
+ if not service_exists(service):
39
+ return "NOT INSTALLED"
40
+
41
+ if service == "pma":
42
+ from ndev.linux.runtime.pma import get_pma_status
43
+ st = get_pma_status()
44
+ if st["running"]:
45
+ return "RUNNING"
46
+ elif not st["installed"]:
47
+ return "NOT INSTALLED"
48
+ else:
49
+ return "STOPPED"
50
+
51
+ if service.startswith("ndev-"):
52
+ version = service[5:]
53
+ script_path = sys.argv[0]
54
+ res = subprocess.run([sys.executable, script_path, "status", version], capture_output=True, text=True)
55
+ if "Running" in res.stdout:
56
+ return "RUNNING"
57
+ else:
58
+ return "STOPPED"
59
+
60
+ if shutil.which("systemctl"):
61
+ res = subprocess.run(["systemctl", "is-active", "--quiet", service])
62
+ return "RUNNING" if res.returncode == 0 else "STOPPED"
63
+ return "UNKNOWN"
64
+
65
+ def get_php_versions() -> list[str]:
66
+ versions = []
67
+ ndev_dir = get_user_ndev_dir() / "php"
68
+ if ndev_dir.exists():
69
+ for d in ndev_dir.iterdir():
70
+ if d.is_dir():
71
+ versions.append(d.name)
72
+ return sorted(versions)
73
+
74
+ def manage_service(service: str, action: str):
75
+ console.print(f"\n[yellow]{action.upper()} -> {service}[/yellow]")
76
+ if service == "pma":
77
+ from ndev.linux.runtime.pma import start_pma, stop_pma, restart_pma
78
+ if action == "start":
79
+ start_pma()
80
+ elif action == "stop":
81
+ stop_pma()
82
+ elif action == "restart":
83
+ restart_pma()
84
+ elif service.startswith("ndev-"):
85
+ version = service[5:]
86
+ script_path = sys.argv[0]
87
+ subprocess.run([sys.executable, script_path, action, version])
88
+ else:
89
+ cmd = ["sudo"]
90
+ if shutil.which("systemctl"):
91
+ cmd.extend(["systemctl", action, service])
92
+ else:
93
+ cmd.extend(["service", service, action])
94
+ subprocess.run(cmd)
95
+ console.print("[green]Done[/green]")
96
+
97
+ def ctl_cmd():
98
+ """Interactive dashboard to start, stop, or restart local web services."""
99
+ console.print("[bold blue]==================================================================[/bold blue]")
100
+ console.print("[bold blue] Web Service Management Tool [/bold blue]")
101
+ console.print("[bold blue]==================================================================[/bold blue]\n")
102
+
103
+ # 1. Detect base services status
104
+ base_services = [("nginx", "Base Service"), ("mariadb", "Base Service"), ("pma", "phpMyAdmin")]
105
+ php_versions = get_php_versions()
106
+
107
+ table = Table(title="Detected Services")
108
+ table.add_column("Service", style="cyan")
109
+ table.add_column("Type", style="magenta")
110
+ table.add_column("Status", style="bold")
111
+
112
+ for svc, svc_type in base_services:
113
+ if service_exists(svc):
114
+ status = service_status(svc)
115
+ color = "green" if status == "RUNNING" else ("yellow" if status == "NOT INSTALLED" else "red")
116
+ table.add_row(svc, svc_type, f"[{color}]{status}[/{color}]")
117
+
118
+ for version in php_versions:
119
+ svc = f"ndev-{version}"
120
+ status = service_status(svc)
121
+ color = "green" if status == "RUNNING" else "red"
122
+ table.add_row(svc, "PHP-FPM", f"[{color}]{status}[/{color}]")
123
+
124
+ console.print(table)
125
+ console.print("")
126
+
127
+ # 2. Select Action
128
+ console.print("[bold]Select Action:[/bold]")
129
+ console.print(" 1) Restart (Default)")
130
+ console.print(" 2) Start")
131
+ console.print(" 3) Stop")
132
+ choice = typer.prompt("Enter choice [1-3]", default=1)
133
+
134
+ action_map = {1: "restart", 2: "start", 3: "stop"}
135
+ action = action_map.get(choice, "restart")
136
+
137
+ # 3. Select Service
138
+ console.print("\n[bold]Select Service:[/bold]")
139
+ console.print(" 1) Nginx")
140
+ console.print(" 2) MariaDB")
141
+ console.print(" 3) phpMyAdmin (pma)")
142
+ console.print(" 4) PHP-FPM")
143
+ console.print(" 5) All Services")
144
+ svc_choice = typer.prompt("Enter choice [1-5]", default=5)
145
+
146
+ php_ver = None
147
+ if svc_choice in [4, 5]:
148
+ if php_versions:
149
+ console.print("\n[bold]Available PHP Versions[/bold]")
150
+ console.print("----------------------")
151
+ for i, version in enumerate(php_versions):
152
+ status = service_status(f"ndev-{version}")
153
+ console.print(f" {i + 1}) PHP {version:<12} {status}")
154
+ console.print(f" {len(php_versions) + 1}) All PHP-FPM Instances")
155
+
156
+ console.print("")
157
+ php_idx = typer.prompt("Select PHP version index", type=int)
158
+ if php_idx < 1 or php_idx > len(php_versions) + 1:
159
+ logger.error("Invalid selection.")
160
+ raise typer.Exit(code=1)
161
+
162
+ if php_idx == len(php_versions) + 1:
163
+ php_ver = "all"
164
+ else:
165
+ php_ver = php_versions[php_idx - 1]
166
+
167
+ services_to_manage = []
168
+ if svc_choice == 1:
169
+ services_to_manage = ["nginx"]
170
+ elif svc_choice == 2:
171
+ services_to_manage = ["mariadb"]
172
+ elif svc_choice == 3:
173
+ services_to_manage = ["pma"]
174
+ elif svc_choice == 4:
175
+ if php_ver == "all":
176
+ services_to_manage = [f"ndev-{v}" for v in php_versions]
177
+ elif php_ver:
178
+ services_to_manage = [f"ndev-{php_ver}"]
179
+ elif svc_choice == 5:
180
+ services_to_manage = ["nginx", "mariadb", "pma"]
181
+ if php_ver:
182
+ if php_ver == "all":
183
+ services_to_manage.extend([f"ndev-{v}" for v in php_versions])
184
+ else:
185
+ services_to_manage.append(f"ndev-{php_ver}")
186
+
187
+ console.print("\n[bold blue]Executing requested actions...[/bold blue]")
188
+ for svc in services_to_manage:
189
+ manage_service(svc, action)
190
+
191
+ console.print("\n[bold green]Completed successfully.[/bold green]")
192
+ console.print("[bold blue]==================================================================[/bold blue]")
@@ -0,0 +1,11 @@
1
+ import typer
2
+ from ndev.common.constants import CURRENT_LINK
3
+ from ndev.common.logger import logger
4
+
5
+ def current_cmd():
6
+ """Show the currently active PHP version."""
7
+ if CURRENT_LINK.exists() and CURRENT_LINK.is_symlink():
8
+ version = CURRENT_LINK.resolve().name
9
+ logger.info(f"Current active PHP version: {version}")
10
+ else:
11
+ logger.info("No active PHP version set. Use 'ndev use <version>' to set one.")