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.
- ndev/__init__.py +8 -0
- ndev/__main__.py +4 -0
- ndev/cli.py +24 -0
- ndev/common/__init__.py +3 -0
- ndev/common/config.py +114 -0
- ndev/common/constants.py +51 -0
- ndev/common/github.py +13 -0
- ndev/common/logger.py +11 -0
- ndev/common/manifest.py +41 -0
- ndev/common/utils.py +96 -0
- ndev/linux/__init__.py +1 -0
- ndev/linux/chroot/manager.py +63 -0
- ndev/linux/chroot/packages.py +91 -0
- ndev/linux/chroot/shell.py +9 -0
- ndev/linux/cli.py +235 -0
- ndev/linux/commands/available.py +38 -0
- ndev/linux/commands/clean.py +25 -0
- ndev/linux/commands/ctl.py +192 -0
- ndev/linux/commands/current.py +11 -0
- ndev/linux/commands/db.py +319 -0
- ndev/linux/commands/doctor.py +56 -0
- ndev/linux/commands/grok.py +75 -0
- ndev/linux/commands/install.py +39 -0
- ndev/linux/commands/list.py +47 -0
- ndev/linux/commands/logs.py +36 -0
- ndev/linux/commands/mailpit.py +82 -0
- ndev/linux/commands/reload.py +26 -0
- ndev/linux/commands/restart.py +34 -0
- ndev/linux/commands/setup.py +113 -0
- ndev/linux/commands/start.py +34 -0
- ndev/linux/commands/status.py +81 -0
- ndev/linux/commands/stop.py +34 -0
- ndev/linux/commands/uninstall.py +69 -0
- ndev/linux/commands/update.py +57 -0
- ndev/linux/commands/upgrade.py +81 -0
- ndev/linux/commands/use.py +108 -0
- ndev/linux/commands/vhost.py +350 -0
- ndev/linux/php/builder.py +183 -0
- ndev/linux/php/downloader.py +58 -0
- ndev/linux/php/extensions.py +146 -0
- ndev/linux/php/installer.py +42 -0
- ndev/linux/php/resolver.py +59 -0
- ndev/linux/php/templates.py +128 -0
- ndev/linux/runtime/fpm.py +117 -0
- ndev/linux/runtime/mailpit.py +244 -0
- ndev/linux/runtime/pma.py +223 -0
- ndev/linux/runtime/process.py +37 -0
- ndev/linux/runtime/sockets.py +16 -0
- ndev/linux/runtime/upgrade.py +431 -0
- ndev/linux/tui.py +1423 -0
- ndev/main.py +52 -0
- ndev/tui.py +23 -0
- ndev/win/__init__.py +1 -0
- ndev/win/cli.py +1898 -0
- ndev/win/commands/__init__.py +0 -0
- ndev/win/core/__init__.py +0 -0
- ndev/win/core/db.py +265 -0
- ndev/win/core/elevate.py +94 -0
- ndev/win/core/ext.py +241 -0
- ndev/win/core/fcgi.py +216 -0
- ndev/win/core/grok.py +55 -0
- ndev/win/core/logs.py +66 -0
- ndev/win/core/mailpit.py +236 -0
- ndev/win/core/mkcert.py +65 -0
- ndev/win/core/paths.py +85 -0
- ndev/win/core/php.py +533 -0
- ndev/win/core/pma.py +190 -0
- ndev/win/core/services.py +349 -0
- ndev/win/core/setup.py +361 -0
- ndev/win/core/upgrade.py +513 -0
- ndev/win/core/vhost.py +289 -0
- ndev/win/templates/vhost.conf.tmpl +33 -0
- ndev/win/templates/vhost_ssl.conf.tmpl +43 -0
- ndev/win/tui.py +1313 -0
- ndev_stack-0.1.0.dist-info/METADATA +553 -0
- ndev_stack-0.1.0.dist-info/RECORD +79 -0
- ndev_stack-0.1.0.dist-info/WHEEL +5 -0
- ndev_stack-0.1.0.dist-info/entry_points.txt +4 -0
- ndev_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from ndev.common.constants import CURRENT_LINK
|
|
3
|
+
from ndev.linux.runtime.fpm import reload_fpm
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def reload_cmd(target: str = typer.Argument(None, help="PHP version or service (e.g. 8.4, pma) to reload")):
|
|
7
|
+
"""Gracefully reload PHP-FPM configuration for a version or restart pma service."""
|
|
8
|
+
if target and target.lower() in ["pma", "phpmyadmin"]:
|
|
9
|
+
from ndev.linux.runtime.pma import restart_pma
|
|
10
|
+
restart_pma()
|
|
11
|
+
return
|
|
12
|
+
from ndev.common.utils import get_version_or_prompt
|
|
13
|
+
try:
|
|
14
|
+
version = get_version_or_prompt(target, "PHP version or service to reload")
|
|
15
|
+
if version and version.lower() in ["pma", "phpmyadmin"]:
|
|
16
|
+
from ndev.linux.runtime.pma import restart_pma
|
|
17
|
+
restart_pma()
|
|
18
|
+
return
|
|
19
|
+
if not version:
|
|
20
|
+
logger.error("No version or service specified.")
|
|
21
|
+
raise typer.Exit(code=1)
|
|
22
|
+
reload_fpm(version)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
logger.error(f"Failed to reload service: {e}")
|
|
25
|
+
raise typer.Exit(code=1)
|
|
26
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from ndev.common.constants import CURRENT_LINK
|
|
3
|
+
from ndev.linux.runtime.fpm import restart_fpm
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def restart_cmd(target: str = typer.Argument(None, help="PHP version or service (e.g. 8.4, pma, mailpit) to restart")):
|
|
7
|
+
"""Restart PHP-FPM for a version or a service like phpmyadmin (pma) or mailpit."""
|
|
8
|
+
if target and target.lower() in ["pma", "phpmyadmin"]:
|
|
9
|
+
from ndev.linux.runtime.pma import restart_pma
|
|
10
|
+
restart_pma()
|
|
11
|
+
return
|
|
12
|
+
if target and target.lower() in ["mailpit", "mail"]:
|
|
13
|
+
from ndev.linux.runtime.mailpit import restart_mailpit
|
|
14
|
+
restart_mailpit()
|
|
15
|
+
return
|
|
16
|
+
from ndev.common.utils import get_version_or_prompt
|
|
17
|
+
try:
|
|
18
|
+
version = get_version_or_prompt(target, "PHP version or service to restart")
|
|
19
|
+
if version and version.lower() in ["pma", "phpmyadmin"]:
|
|
20
|
+
from ndev.linux.runtime.pma import restart_pma
|
|
21
|
+
restart_pma()
|
|
22
|
+
return
|
|
23
|
+
if version and version.lower() in ["mailpit", "mail"]:
|
|
24
|
+
from ndev.linux.runtime.mailpit import restart_mailpit
|
|
25
|
+
restart_mailpit()
|
|
26
|
+
return
|
|
27
|
+
if not version:
|
|
28
|
+
logger.error("No version or service specified.")
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
restart_fpm(version)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Failed to restart service: {e}")
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import typer
|
|
6
|
+
import httpx
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from ndev.common.constants import CURRENT_LINK
|
|
10
|
+
from ndev.common.logger import logger
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
def chown_to_sudo_user(path: Path):
|
|
15
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
16
|
+
if sudo_user:
|
|
17
|
+
try:
|
|
18
|
+
import pwd
|
|
19
|
+
pw = pwd.getpwnam(sudo_user)
|
|
20
|
+
os.chown(str(path), pw.pw_uid, pw.pw_gid)
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def get_user_local_bin_dir() -> Path:
|
|
25
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
26
|
+
if sudo_user:
|
|
27
|
+
try:
|
|
28
|
+
import pwd
|
|
29
|
+
return Path(pwd.getpwnam(sudo_user).pw_dir) / ".local" / "bin"
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return Path(os.path.expanduser("~/.local/bin"))
|
|
33
|
+
|
|
34
|
+
def setup_cmd():
|
|
35
|
+
"""Install MariaDB, Nginx, and Composer on the system (elevates with sudo for system packages)."""
|
|
36
|
+
if not shutil.which("apt-get"):
|
|
37
|
+
logger.error("This setup command is only supported on Debian-based systems with apt.")
|
|
38
|
+
raise typer.Exit(code=1)
|
|
39
|
+
|
|
40
|
+
if not shutil.which("sudo"):
|
|
41
|
+
logger.error("sudo command not found. This command requires sudo to install system packages.")
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 1. Update Package Lists
|
|
46
|
+
console.print("[bold yellow]Updating package lists (sudo apt-get update)...[/bold yellow]")
|
|
47
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
|
48
|
+
|
|
49
|
+
# 2. Install MariaDB and Nginx
|
|
50
|
+
console.print("\n[bold yellow]Installing MariaDB and Nginx (sudo apt-get install)...[/bold yellow]")
|
|
51
|
+
subprocess.run(["sudo", "apt-get", "install", "-y", "mariadb-server", "nginx", "curl"], check=True)
|
|
52
|
+
console.print("[bold green]MariaDB and Nginx installed successfully![/bold green]\n")
|
|
53
|
+
|
|
54
|
+
# 3. Install Composer
|
|
55
|
+
local_bin = get_user_local_bin_dir()
|
|
56
|
+
composer_bin = local_bin / "composer"
|
|
57
|
+
if composer_bin.exists():
|
|
58
|
+
console.print(f"[bold green]Composer is already installed at {composer_bin}[/bold green]")
|
|
59
|
+
else:
|
|
60
|
+
# Find PHP binary
|
|
61
|
+
php_bin = None
|
|
62
|
+
if CURRENT_LINK.exists() or CURRENT_LINK.is_symlink():
|
|
63
|
+
potential_php = CURRENT_LINK / "bin" / "php"
|
|
64
|
+
if potential_php.exists():
|
|
65
|
+
php_bin = potential_php
|
|
66
|
+
if not php_bin:
|
|
67
|
+
system_php = shutil.which("php")
|
|
68
|
+
if system_php:
|
|
69
|
+
php_bin = Path(system_php)
|
|
70
|
+
|
|
71
|
+
if not php_bin:
|
|
72
|
+
console.print("[bold yellow]PHP is not installed on the system (neither active ndev PHP nor system php).[/bold yellow]")
|
|
73
|
+
console.print("[bold yellow]Skipping Composer installation. Please install a PHP version first using 'ndev install <version>' and run setup again.[/bold yellow]")
|
|
74
|
+
else:
|
|
75
|
+
console.print("[bold yellow]Installing Composer...[/bold yellow]")
|
|
76
|
+
installer_url = "https://getcomposer.org/installer"
|
|
77
|
+
res = httpx.get(installer_url, follow_redirects=True)
|
|
78
|
+
if res.status_code != 200:
|
|
79
|
+
raise RuntimeError(f"Failed to fetch Composer installer (HTTP status {res.status_code})")
|
|
80
|
+
|
|
81
|
+
setup_php = Path("/tmp/composer-setup.php")
|
|
82
|
+
setup_php.write_text(res.text)
|
|
83
|
+
|
|
84
|
+
local_bin.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
chown_to_sudo_user(local_bin)
|
|
86
|
+
|
|
87
|
+
subprocess.run([str(php_bin), str(setup_php), f"--install-dir={local_bin}", "--filename=composer"], check=True)
|
|
88
|
+
if setup_php.exists():
|
|
89
|
+
setup_php.unlink()
|
|
90
|
+
|
|
91
|
+
chown_to_sudo_user(composer_bin)
|
|
92
|
+
console.print(f"[bold green]Composer installed successfully under {composer_bin}[/bold green]")
|
|
93
|
+
|
|
94
|
+
# 4. Create ndev symlink in ~/.local/bin/ndev pointing to the virtualenv ndev executable
|
|
95
|
+
ndev_venv_path = Path(sys.executable).parent / "ndev"
|
|
96
|
+
if ndev_venv_path.exists():
|
|
97
|
+
ndev_link = local_bin / "ndev"
|
|
98
|
+
if ndev_link.exists() or ndev_link.is_symlink():
|
|
99
|
+
ndev_link.unlink()
|
|
100
|
+
local_bin.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
chown_to_sudo_user(local_bin)
|
|
102
|
+
ndev_link.symlink_to(ndev_venv_path)
|
|
103
|
+
chown_to_sudo_user(ndev_link)
|
|
104
|
+
console.print(f"[bold green]Created symlink {ndev_link} -> {ndev_venv_path}[/bold green]")
|
|
105
|
+
|
|
106
|
+
console.print("\n[bold green]System setup completed successfully![/bold green]")
|
|
107
|
+
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
logger.error(f"Command execution failed: {e}")
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Setup failed: {e}")
|
|
113
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from ndev.common.constants import CURRENT_LINK
|
|
3
|
+
from ndev.linux.runtime.fpm import start_fpm
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def start_cmd(target: str = typer.Argument(None, help="PHP version or service (e.g. 8.4, pma, mailpit) to start")):
|
|
7
|
+
"""Start PHP-FPM for a version or a service like phpmyadmin (pma) or mailpit."""
|
|
8
|
+
if target and target.lower() in ["pma", "phpmyadmin"]:
|
|
9
|
+
from ndev.linux.runtime.pma import start_pma
|
|
10
|
+
start_pma()
|
|
11
|
+
return
|
|
12
|
+
if target and target.lower() in ["mailpit", "mail"]:
|
|
13
|
+
from ndev.linux.runtime.mailpit import start_mailpit
|
|
14
|
+
start_mailpit()
|
|
15
|
+
return
|
|
16
|
+
from ndev.common.utils import get_version_or_prompt
|
|
17
|
+
try:
|
|
18
|
+
version = get_version_or_prompt(target, "PHP version or service to start")
|
|
19
|
+
if version and version.lower() in ["pma", "phpmyadmin"]:
|
|
20
|
+
from ndev.linux.runtime.pma import start_pma
|
|
21
|
+
start_pma()
|
|
22
|
+
return
|
|
23
|
+
if version and version.lower() in ["mailpit", "mail"]:
|
|
24
|
+
from ndev.linux.runtime.mailpit import start_mailpit
|
|
25
|
+
start_mailpit()
|
|
26
|
+
return
|
|
27
|
+
if not version:
|
|
28
|
+
logger.error("No version or service specified.")
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
start_fpm(version)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Failed to start service: {e}")
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from ndev.common.constants import CURRENT_LINK
|
|
5
|
+
from ndev.linux.runtime.fpm import get_fpm_status
|
|
6
|
+
from ndev.common.logger import logger
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
def status_cmd(target: str = typer.Argument(None, help="PHP version or service (e.g. 8.4, pma, mailpit) to check status for")):
|
|
11
|
+
"""Check status of a PHP-FPM version or service like pma or mailpit."""
|
|
12
|
+
if target and target.lower() in ["pma", "phpmyadmin"]:
|
|
13
|
+
from ndev.linux.runtime.pma import get_pma_status
|
|
14
|
+
status = get_pma_status()
|
|
15
|
+
table = Table(title="phpMyAdmin Service Status")
|
|
16
|
+
table.add_column("Property", style="bold cyan")
|
|
17
|
+
table.add_column("Value")
|
|
18
|
+
|
|
19
|
+
status_text = "[bold green]Running[/bold green]" if status["running"] else "[bold red]Stopped[/bold red]"
|
|
20
|
+
table.add_row("Service", "phpMyAdmin (pma)")
|
|
21
|
+
table.add_row("Status", status_text)
|
|
22
|
+
table.add_row("PID", str(status["pid"]) if status["pid"] else "N/A")
|
|
23
|
+
table.add_row("Port", str(status["port"]) if status["port"] else "N/A")
|
|
24
|
+
table.add_row("URL", status["url"] if status["url"] else "N/A")
|
|
25
|
+
console.print(table)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if target and target.lower() in ["mailpit", "mail"]:
|
|
29
|
+
from ndev.linux.runtime.mailpit import get_mailpit_status
|
|
30
|
+
status = get_mailpit_status()
|
|
31
|
+
table = Table(title="Mailpit Service Status")
|
|
32
|
+
table.add_column("Property", style="bold cyan")
|
|
33
|
+
table.add_column("Value")
|
|
34
|
+
|
|
35
|
+
status_text = "[bold green]Running[/bold green]" if status["running"] else "[bold red]Stopped[/bold red]"
|
|
36
|
+
table.add_row("Service", "Mailpit")
|
|
37
|
+
table.add_row("Status", status_text)
|
|
38
|
+
table.add_row("PID", str(status["pid"]) if status["pid"] else "N/A")
|
|
39
|
+
table.add_row("SMTP Server", f"127.0.0.1:{status['smtp_port']}")
|
|
40
|
+
table.add_row("Web UI URL", status["url"] if status["url"] else f"http://127.0.0.1:{status['web_port']} (Stopped)")
|
|
41
|
+
console.print(table)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
from ndev.common.utils import get_version_or_prompt
|
|
45
|
+
try:
|
|
46
|
+
version = get_version_or_prompt(target, "PHP version or service to check status")
|
|
47
|
+
if version and version.lower() in ["pma", "phpmyadmin"]:
|
|
48
|
+
from ndev.linux.runtime.pma import get_pma_status
|
|
49
|
+
status = get_pma_status()
|
|
50
|
+
table = Table(title="phpMyAdmin Service Status")
|
|
51
|
+
table.add_column("Property", style="bold cyan")
|
|
52
|
+
table.add_column("Value")
|
|
53
|
+
status_text = "[bold green]Running[/bold green]" if status["running"] else "[bold red]Stopped[/bold red]"
|
|
54
|
+
table.add_row("Service", "phpMyAdmin (pma)")
|
|
55
|
+
table.add_row("Status", status_text)
|
|
56
|
+
table.add_row("PID", str(status["pid"]) if status["pid"] else "N/A")
|
|
57
|
+
table.add_row("Port", str(status["port"]) if status["port"] else "N/A")
|
|
58
|
+
table.add_row("URL", status["url"] if status["url"] else "N/A")
|
|
59
|
+
console.print(table)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if not version:
|
|
63
|
+
logger.error("No version or service specified.")
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
status = get_fpm_status(version)
|
|
66
|
+
|
|
67
|
+
table = Table(title=f"PHP-FPM {version} Status")
|
|
68
|
+
table.add_column("Property", style="bold cyan")
|
|
69
|
+
table.add_column("Value")
|
|
70
|
+
|
|
71
|
+
status_text = "[bold green]Running[/bold green]" if status["running"] else "[bold red]Stopped[/bold red]"
|
|
72
|
+
table.add_row("Version", status["version"])
|
|
73
|
+
table.add_row("Status", status_text)
|
|
74
|
+
table.add_row("PID", str(status["pid"]) if status["pid"] else "N/A")
|
|
75
|
+
table.add_row("Socket Path", status["socket"])
|
|
76
|
+
table.add_row("Socket Active", "[green]Yes[/green]" if status["socket_exists"] else "[yellow]No[/yellow]")
|
|
77
|
+
|
|
78
|
+
console.print(table)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to get service status: {e}")
|
|
81
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from ndev.common.constants import CURRENT_LINK
|
|
3
|
+
from ndev.linux.runtime.fpm import stop_fpm
|
|
4
|
+
from ndev.common.logger import logger
|
|
5
|
+
|
|
6
|
+
def stop_cmd(target: str = typer.Argument(None, help="PHP version or service (e.g. 8.4, pma, mailpit) to stop")):
|
|
7
|
+
"""Stop PHP-FPM for a version or a service like phpmyadmin (pma) or mailpit."""
|
|
8
|
+
if target and target.lower() in ["pma", "phpmyadmin"]:
|
|
9
|
+
from ndev.linux.runtime.pma import stop_pma
|
|
10
|
+
stop_pma()
|
|
11
|
+
return
|
|
12
|
+
if target and target.lower() in ["mailpit", "mail"]:
|
|
13
|
+
from ndev.linux.runtime.mailpit import stop_mailpit
|
|
14
|
+
stop_mailpit()
|
|
15
|
+
return
|
|
16
|
+
from ndev.common.utils import get_version_or_prompt
|
|
17
|
+
try:
|
|
18
|
+
version = get_version_or_prompt(target, "PHP version or service to stop")
|
|
19
|
+
if version and version.lower() in ["pma", "phpmyadmin"]:
|
|
20
|
+
from ndev.linux.runtime.pma import stop_pma
|
|
21
|
+
stop_pma()
|
|
22
|
+
return
|
|
23
|
+
if version and version.lower() in ["mailpit", "mail"]:
|
|
24
|
+
from ndev.linux.runtime.mailpit import stop_mailpit
|
|
25
|
+
stop_mailpit()
|
|
26
|
+
return
|
|
27
|
+
if not version:
|
|
28
|
+
logger.error("No version or service specified.")
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
stop_fpm(version)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Failed to stop service: {e}")
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import typer
|
|
3
|
+
from ndev.common.constants import PHP_DIR, CURRENT_LINK
|
|
4
|
+
from ndev.linux.runtime.fpm import stop_fpm
|
|
5
|
+
from ndev.common.manifest import remove_installed_version
|
|
6
|
+
from ndev.common.logger import logger
|
|
7
|
+
|
|
8
|
+
def uninstall_cmd(version: str = typer.Argument(None, help="PHP version to uninstall (e.g. 8.4.23)")):
|
|
9
|
+
"""Uninstall a compiled PHP version."""
|
|
10
|
+
if not version:
|
|
11
|
+
# Check if there are installed versions to list
|
|
12
|
+
installed_versions = []
|
|
13
|
+
if PHP_DIR.exists():
|
|
14
|
+
for path in PHP_DIR.iterdir():
|
|
15
|
+
if path.is_dir():
|
|
16
|
+
installed_versions.append(path.name)
|
|
17
|
+
if installed_versions:
|
|
18
|
+
from packaging.version import parse as parse_version
|
|
19
|
+
try:
|
|
20
|
+
installed_versions = sorted(installed_versions, key=parse_version)
|
|
21
|
+
except Exception:
|
|
22
|
+
installed_versions = sorted(installed_versions)
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
console = Console()
|
|
25
|
+
console.print("\n[bold]Installed PHP Versions[/bold]")
|
|
26
|
+
console.print("----------------------")
|
|
27
|
+
for i, v in enumerate(installed_versions):
|
|
28
|
+
console.print(f" {i + 1}) {v}")
|
|
29
|
+
console.print("")
|
|
30
|
+
try:
|
|
31
|
+
choice = typer.prompt("Select PHP version index or enter version directly", default="1")
|
|
32
|
+
try:
|
|
33
|
+
idx = int(choice)
|
|
34
|
+
if 1 <= idx <= len(installed_versions):
|
|
35
|
+
version = installed_versions[idx - 1]
|
|
36
|
+
except ValueError:
|
|
37
|
+
version = choice.strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
if not version:
|
|
41
|
+
version = typer.prompt("PHP version to uninstall").strip()
|
|
42
|
+
|
|
43
|
+
if not version:
|
|
44
|
+
logger.error("PHP version is required.")
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
|
|
47
|
+
prefix = PHP_DIR / version
|
|
48
|
+
|
|
49
|
+
if not prefix.exists():
|
|
50
|
+
logger.error(f"PHP version {version} is not installed.")
|
|
51
|
+
raise typer.Exit(code=1)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
stop_fpm(version)
|
|
55
|
+
|
|
56
|
+
logger.info(f"Removing files at {prefix}...")
|
|
57
|
+
shutil.rmtree(prefix)
|
|
58
|
+
|
|
59
|
+
remove_installed_version(version)
|
|
60
|
+
|
|
61
|
+
if CURRENT_LINK.exists() and CURRENT_LINK.is_symlink():
|
|
62
|
+
if CURRENT_LINK.resolve() == prefix.resolve():
|
|
63
|
+
logger.info("Removing symlink to current version.")
|
|
64
|
+
CURRENT_LINK.unlink()
|
|
65
|
+
|
|
66
|
+
logger.info(f"PHP version {version} uninstalled successfully.")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Uninstallation failed: {e}")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from packaging.version import parse as parse_version
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from ndev.common.constants import PHP_DIR
|
|
6
|
+
from ndev.common.github import fetch_releases
|
|
7
|
+
from ndev.common.logger import logger
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
def update_cmd():
|
|
12
|
+
"""Check if installed PHP versions have newer patch releases available."""
|
|
13
|
+
if not PHP_DIR.exists():
|
|
14
|
+
logger.info("No PHP versions installed yet.")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
installed_versions = []
|
|
18
|
+
for path in PHP_DIR.iterdir():
|
|
19
|
+
if path.is_dir():
|
|
20
|
+
installed_versions.append(path.name)
|
|
21
|
+
|
|
22
|
+
if not installed_versions:
|
|
23
|
+
logger.info("No PHP versions installed yet.")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
logger.info("Checking for newer patch releases...")
|
|
27
|
+
|
|
28
|
+
table = Table(title="PHP Update Check")
|
|
29
|
+
table.add_column("Installed Version", style="bold cyan")
|
|
30
|
+
table.add_column("Latest Available")
|
|
31
|
+
table.add_column("Status")
|
|
32
|
+
|
|
33
|
+
for v in installed_versions:
|
|
34
|
+
parts = v.split(".")
|
|
35
|
+
try:
|
|
36
|
+
major = int(parts[0])
|
|
37
|
+
minor_prefix = f"{parts[0]}.{parts[1]}."
|
|
38
|
+
except (ValueError, IndexError):
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
releases = fetch_releases(major)
|
|
42
|
+
if not releases:
|
|
43
|
+
table.add_row(v, "Unknown", "[yellow]Network Error[/yellow]")
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
matching = [rel for rel in releases.keys() if rel.startswith(minor_prefix)]
|
|
47
|
+
if not matching:
|
|
48
|
+
table.add_row(v, v, "Up to date")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
latest = max(matching, key=parse_version)
|
|
52
|
+
if parse_version(latest) > parse_version(v):
|
|
53
|
+
table.add_row(v, latest, f"[bold yellow]Update Available[/bold yellow] (run 'ndev install {latest}')")
|
|
54
|
+
else:
|
|
55
|
+
table.add_row(v, v, "Up to date")
|
|
56
|
+
|
|
57
|
+
console.print(table)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`ndev upgrade` command for Linux stack components.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ndev.linux.runtime import upgrade as upgrade_rt
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Check for and upgrade stack components (Nginx, Mailpit, MariaDB, PMA, mkcert, Composer).")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.callback(invoke_without_command=True)
|
|
18
|
+
def main(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
component: Optional[str] = typer.Argument(None, help="Component to upgrade (nginx, mailpit, mariadb, pma, mkcert, composer, or all)"),
|
|
21
|
+
check: bool = typer.Option(False, "--check", "-c", help="Only check for updates without applying upgrades."),
|
|
22
|
+
):
|
|
23
|
+
if ctx.invoked_subcommand is not None:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
console.print("\n[bold blue]ndev Linux Stack Component Updates & Upgrades[/bold blue]")
|
|
27
|
+
|
|
28
|
+
with console.status("[bold green]Checking component versions...[/bold green]"):
|
|
29
|
+
if component and component.lower() != "all":
|
|
30
|
+
infos = [info for info in upgrade_rt.check_all() if info.name == component.lower() or component.lower() in info.name]
|
|
31
|
+
if not infos:
|
|
32
|
+
console.print(f"[bold red]Unknown component '{component}'. Available: {', '.join(upgrade_rt.COMPONENTS)}[/bold red]")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
else:
|
|
35
|
+
infos = upgrade_rt.check_all()
|
|
36
|
+
|
|
37
|
+
table = Table(title="Stack Components Version Status", show_header=True, header_style="bold cyan")
|
|
38
|
+
table.add_column("Component", style="bold", min_width=20)
|
|
39
|
+
table.add_column("Installed Version", min_width=18)
|
|
40
|
+
table.add_column("Latest Version", min_width=18)
|
|
41
|
+
table.add_column("Status", min_width=22)
|
|
42
|
+
|
|
43
|
+
upgradable = []
|
|
44
|
+
for info in infos:
|
|
45
|
+
curr_str = info.current_version or "[dim]Not installed[/dim]"
|
|
46
|
+
latest_str = info.latest_version or "[dim]Unknown[/dim]"
|
|
47
|
+
if not info.installed:
|
|
48
|
+
st_str = "[yellow]Not Installed[/yellow]"
|
|
49
|
+
elif info.update_available:
|
|
50
|
+
st_str = "[bold green]Update Available[/bold green]"
|
|
51
|
+
upgradable.append(info)
|
|
52
|
+
else:
|
|
53
|
+
st_str = "[green]Up-to-date[/green]"
|
|
54
|
+
|
|
55
|
+
table.add_row(info.display_name, curr_str, latest_str, st_str)
|
|
56
|
+
|
|
57
|
+
console.print(table)
|
|
58
|
+
|
|
59
|
+
if check:
|
|
60
|
+
if upgradable:
|
|
61
|
+
console.print(f"\n[bold green]{len(upgradable)} component(s) can be upgraded.[/bold green] Run `ndev upgrade` to apply.")
|
|
62
|
+
else:
|
|
63
|
+
console.print("\n[bold green]All installed components are up-to-date![/bold green]")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if not upgradable and not component:
|
|
67
|
+
console.print("\n[bold green]All installed components are up-to-date![/bold green]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
targets = [c.name for c in upgradable] if not component or component.lower() == "all" else [component.lower()]
|
|
71
|
+
console.print(f"\n[bold blue]Upgrading {len(targets)} component(s): {', '.join(targets)}...[/bold blue]\n")
|
|
72
|
+
|
|
73
|
+
for target in targets:
|
|
74
|
+
with console.status(f"[bold green]Upgrading {target}...[/bold green]"):
|
|
75
|
+
ok, msg = upgrade_rt.upgrade_component(target)
|
|
76
|
+
if ok:
|
|
77
|
+
console.print(f"[bold green]✓ {msg}[/bold green]")
|
|
78
|
+
else:
|
|
79
|
+
console.print(f"[bold red]✗ {msg}[/bold red]")
|
|
80
|
+
|
|
81
|
+
console.print("\n[bold green]Upgrade process complete![/bold green]")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ndev.common.constants import PHP_DIR, CURRENT_LINK
|
|
6
|
+
from ndev.common.logger import logger
|
|
7
|
+
|
|
8
|
+
def use_cmd(version: str = typer.Argument(None, help="PHP version to use (must be installed)")):
|
|
9
|
+
"""Set a PHP version as the active version."""
|
|
10
|
+
if not version:
|
|
11
|
+
# Check if there are installed versions to list
|
|
12
|
+
installed_versions = []
|
|
13
|
+
if PHP_DIR.exists():
|
|
14
|
+
for path in PHP_DIR.iterdir():
|
|
15
|
+
if path.is_dir():
|
|
16
|
+
installed_versions.append(path.name)
|
|
17
|
+
if installed_versions:
|
|
18
|
+
from packaging.version import parse as parse_version
|
|
19
|
+
try:
|
|
20
|
+
installed_versions = sorted(installed_versions, key=parse_version)
|
|
21
|
+
except Exception:
|
|
22
|
+
installed_versions = sorted(installed_versions)
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
console = Console()
|
|
25
|
+
console.print("\n[bold]Installed PHP Versions[/bold]")
|
|
26
|
+
console.print("----------------------")
|
|
27
|
+
for i, v in enumerate(installed_versions):
|
|
28
|
+
console.print(f" {i + 1}) {v}")
|
|
29
|
+
console.print("")
|
|
30
|
+
try:
|
|
31
|
+
choice = typer.prompt("Select PHP version index or enter version directly", default="1")
|
|
32
|
+
try:
|
|
33
|
+
idx = int(choice)
|
|
34
|
+
if 1 <= idx <= len(installed_versions):
|
|
35
|
+
version = installed_versions[idx - 1]
|
|
36
|
+
except ValueError:
|
|
37
|
+
version = choice.strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
if not version:
|
|
41
|
+
version = typer.prompt("PHP version to use").strip()
|
|
42
|
+
|
|
43
|
+
if not version:
|
|
44
|
+
logger.error("PHP version is required.")
|
|
45
|
+
raise typer.Exit(code=1)
|
|
46
|
+
|
|
47
|
+
target = PHP_DIR / version
|
|
48
|
+
|
|
49
|
+
if not target.exists():
|
|
50
|
+
logger.error(f"PHP version {version} is not installed. Install it first using 'ndev install {version}'.")
|
|
51
|
+
raise typer.Exit(code=1)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
if CURRENT_LINK.exists() or CURRENT_LINK.is_symlink():
|
|
55
|
+
CURRENT_LINK.unlink()
|
|
56
|
+
CURRENT_LINK.symlink_to(target)
|
|
57
|
+
|
|
58
|
+
# Ensure ~/.local/bin symlinks exist
|
|
59
|
+
local_bin = Path(os.path.expanduser("~/.local/bin"))
|
|
60
|
+
local_bin.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
links = {
|
|
63
|
+
"php": CURRENT_LINK / "bin" / "php",
|
|
64
|
+
"phpize": CURRENT_LINK / "bin" / "phpize",
|
|
65
|
+
"php-config": CURRENT_LINK / "bin" / "php-config",
|
|
66
|
+
"php-fpm": CURRENT_LINK / "sbin" / "php-fpm"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for name, target_path in links.items():
|
|
70
|
+
link_path = local_bin / name
|
|
71
|
+
if link_path.exists() or link_path.is_symlink():
|
|
72
|
+
link_path.unlink()
|
|
73
|
+
link_path.symlink_to(target_path)
|
|
74
|
+
|
|
75
|
+
logger.info(f"Now using PHP version {version}")
|
|
76
|
+
|
|
77
|
+
# Check if ~/.local/bin is in PATH
|
|
78
|
+
path_env = os.environ.get("PATH", "")
|
|
79
|
+
local_bin_str = str(local_bin)
|
|
80
|
+
in_path = any(
|
|
81
|
+
p == local_bin_str or Path(p).resolve() == local_bin.resolve()
|
|
82
|
+
for p in path_env.split(os.pathsep)
|
|
83
|
+
)
|
|
84
|
+
if not in_path:
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"[yellow]Warning: {local_bin} is not in your PATH. [/yellow]"
|
|
87
|
+
f"You may need to add it to your shell configuration (e.g. ~/.bashrc or ~/.zshrc):\n"
|
|
88
|
+
f' export PATH="$HOME/.local/bin:$PATH"'
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
# Verify if the active php command resolves to the ndev symlink
|
|
92
|
+
php_path = shutil.which("php")
|
|
93
|
+
if php_path:
|
|
94
|
+
try:
|
|
95
|
+
resolved_php = Path(php_path).resolve()
|
|
96
|
+
expected_php = (local_bin / "php").resolve()
|
|
97
|
+
if resolved_php != expected_php:
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"[yellow]Warning: The active PHP command resolves to '{resolved_php}' [/yellow]\n"
|
|
100
|
+
f"which is not the ndev symlink '{expected_php}'.\n"
|
|
101
|
+
f"Please ensure '{local_bin}' appears before other PHP installations in your PATH."
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Failed to switch PHP version: {e}")
|
|
108
|
+
raise typer.Exit(code=1)
|