ndev-stack 0.3.15__tar.gz → 0.3.17__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.15 → ndev_stack-0.3.17}/PKG-INFO +1 -1
  2. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/__init__.py +1 -1
  3. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/db.py +196 -10
  4. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/nginx_helper.py +1 -0
  5. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/PKG-INFO +1 -1
  6. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/pyproject.toml +1 -1
  7. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/tests/test_opensuse_compat.py +28 -0
  8. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/README.md +0 -0
  9. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/__main__.py +0 -0
  10. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/cli.py +0 -0
  11. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/__init__.py +0 -0
  12. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/config.py +0 -0
  13. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/constants.py +0 -0
  14. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/distro.py +0 -0
  15. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/github.py +0 -0
  16. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/logger.py +0 -0
  17. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/manifest.py +0 -0
  18. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/__init__.py +0 -0
  19. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/base.py +0 -0
  20. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  21. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  22. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  23. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/mongodb/module.py +0 -0
  24. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  25. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/postgres/module.py +0 -0
  26. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  27. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/builtin/redis/module.py +0 -0
  28. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/modules/manager.py +0 -0
  29. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/common/utils.py +0 -0
  30. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/__init__.py +0 -0
  31. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/chroot/manager.py +0 -0
  32. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/chroot/packages.py +0 -0
  33. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/chroot/shell.py +0 -0
  34. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/cli.py +0 -0
  35. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/available.py +0 -0
  36. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/clean.py +0 -0
  37. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/ctl.py +0 -0
  38. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/current.py +0 -0
  39. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/doctor.py +0 -0
  40. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/grok.py +0 -0
  41. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/install.py +0 -0
  42. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/list.py +0 -0
  43. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/logs.py +0 -0
  44. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/mailpit.py +0 -0
  45. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/modules.py +0 -0
  46. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/mongodb.py +0 -0
  47. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/postgres.py +0 -0
  48. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/reload.py +0 -0
  49. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/restart.py +0 -0
  50. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/setup.py +0 -0
  51. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/start.py +0 -0
  52. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/status.py +0 -0
  53. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/stop.py +0 -0
  54. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/uninstall.py +0 -0
  55. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/update.py +0 -0
  56. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/upgrade.py +0 -0
  57. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/use.py +0 -0
  58. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/commands/vhost.py +0 -0
  59. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/builder.py +0 -0
  60. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/downloader.py +0 -0
  61. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/extensions.py +0 -0
  62. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/installer.py +0 -0
  63. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/resolver.py +0 -0
  64. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/php/templates.py +0 -0
  65. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/fpm.py +0 -0
  66. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/mailpit.py +0 -0
  67. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/pma.py +0 -0
  68. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/process.py +0 -0
  69. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/services.py +0 -0
  70. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/sockets.py +0 -0
  71. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/runtime/upgrade.py +0 -0
  72. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/linux/tui.py +0 -0
  73. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/main.py +0 -0
  74. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/tui.py +0 -0
  75. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/__init__.py +0 -0
  76. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/bin/shim.exe +0 -0
  77. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/cli.py +0 -0
  78. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/commands/__init__.py +0 -0
  79. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/__init__.py +0 -0
  80. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/db.py +0 -0
  81. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/elevate.py +0 -0
  82. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/ext.py +0 -0
  83. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/fcgi.py +0 -0
  84. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/grok.py +0 -0
  85. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/logs.py +0 -0
  86. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/mailpit.py +0 -0
  87. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/mkcert.py +0 -0
  88. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/paths.py +0 -0
  89. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/php.py +0 -0
  90. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/pma.py +0 -0
  91. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/redis_core.py +0 -0
  92. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/services.py +0 -0
  93. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/setup.py +0 -0
  94. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/shim.py +0 -0
  95. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/upgrade.py +0 -0
  96. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/core/vhost.py +0 -0
  97. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/templates/vhost.conf.tmpl +0 -0
  98. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  99. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev/win/tui.py +0 -0
  100. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/SOURCES.txt +0 -0
  101. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/dependency_links.txt +0 -0
  102. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/entry_points.txt +0 -0
  103. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/requires.txt +0 -0
  104. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/ndev_stack.egg-info/top_level.txt +0 -0
  105. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/setup.cfg +0 -0
  106. {ndev_stack-0.3.15 → ndev_stack-0.3.17}/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.15
3
+ Version: 0.3.17
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.15"
4
+ __version__ = "0.3.17"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -3,9 +3,11 @@ import sys
3
3
  import shutil
4
4
  import subprocess
5
5
  import typer
6
+ from pathlib import Path
6
7
  from typing import Optional
7
8
  from rich.console import Console
8
9
  from ndev.common.logger import logger
10
+ from ndev.common.utils import which_binary
9
11
 
10
12
  console = Console()
11
13
 
@@ -13,7 +15,8 @@ db_app = typer.Typer(help="Manage MySQL databases and users", invoke_without_com
13
15
 
14
16
  # Shared options for commands
15
17
  def execute_sql_mysql(host: str, port: int, user: str, password: Optional[str], sql: str) -> subprocess.CompletedProcess:
16
- cmd = ["mysql", "-h", host, "-P", str(port), "-u", user, "--batch", "--skip-column-names"]
18
+ client = which_binary("mariadb") or which_binary("mysql") or "mysql"
19
+ cmd = [client, "-h", host, "-P", str(port), "-u", user, "--batch", "--skip-column-names"]
17
20
  if password:
18
21
  cmd.append(f"--password={password}")
19
22
  return subprocess.run(cmd, input=sql, capture_output=True, text=True)
@@ -44,13 +47,20 @@ def run_wizard():
44
47
  console.print(" 1) Create Database")
45
48
  console.print(" 2) Drop Database")
46
49
  console.print(" 3) Export Database (mysqldump)")
47
- console.print(" 4) Create User")
48
- console.print(" 5) Drop User")
49
- op_choice = typer.prompt("Choice [1-5]", default=1, type=int)
50
+ console.print(" 4) Import Database (.sql file)")
51
+ console.print(" 5) Create User")
52
+ console.print(" 6) Drop User")
53
+ op_choice = typer.prompt("Choice [1-6]", default=1, type=int)
50
54
 
51
- action_map = {1: "create-db", 2: "drop-db", 3: "export-db", 4: "create-user", 5: "drop-user"}
55
+ action_map = {1: "create-db", 2: "drop-db", 3: "export-db", 4: "import-db", 5: "create-user", 6: "drop-user"}
52
56
  action = action_map.get(op_choice, "create-db")
53
57
 
58
+ if action == "import-db":
59
+ sql_file = typer.prompt("Path to .sql file to import").strip()
60
+ target_db = typer.prompt("Database name (optional, press Enter to skip)", default="")
61
+ execute_import_db(host, port, admin_user, admin_password, sql_file, target_db or None)
62
+ return
63
+
54
64
  target_name = typer.prompt("Database/Username")
55
65
  if not target_name:
56
66
  logger.error("Target name is required.")
@@ -160,6 +170,66 @@ def execute_export_db(
160
170
  raise typer.Exit(code=1)
161
171
  sys.stdout.write(res.stdout)
162
172
 
173
+ def execute_import_db(
174
+ host: str,
175
+ port: int,
176
+ user: str,
177
+ password: Optional[str],
178
+ sql_file: str,
179
+ database: Optional[str] = None,
180
+ ):
181
+ sql_path = Path(sql_file).expanduser().resolve()
182
+ if not sql_path.exists():
183
+ logger.error(f"SQL file not found: {sql_path}")
184
+ raise typer.Exit(code=1)
185
+
186
+ client = which_binary("mariadb") or which_binary("mysql") or "mysql"
187
+ if not shutil.which(client):
188
+ logger.error(f"MySQL / MariaDB client ({client}) not found in PATH.")
189
+ raise typer.Exit(code=1)
190
+
191
+ # Resolve password if not provided
192
+ if password is None and user == "root":
193
+ res = subprocess.run([client, "-h", host, "-P", str(port), "-u", "root", "--password=root", "-e", "SELECT 1;"], capture_output=True)
194
+ if res.returncode == 0:
195
+ password = "root"
196
+ else:
197
+ res_blank = subprocess.run([client, "-h", host, "-P", str(port), "-u", "root", "-e", "SELECT 1;"], capture_output=True)
198
+ if res_blank.returncode == 0:
199
+ password = ""
200
+ else:
201
+ password = "root"
202
+
203
+ # If target database specified, ensure it exists
204
+ if database:
205
+ create_sql = f"CREATE DATABASE IF NOT EXISTS `{database}`;"
206
+ res_create = execute_sql_mysql(host, port, user, password, create_sql)
207
+ if res_create.returncode != 0:
208
+ logger.error(f"Failed to ensure database '{database}' exists: {res_create.stderr}")
209
+ raise typer.Exit(code=1)
210
+
211
+ cmd = [client, "-h", host, "-P", str(port), "-u", user]
212
+ if password:
213
+ cmd.append(f"--password={password}")
214
+ if database:
215
+ cmd.append(database)
216
+
217
+ target_desc = f"database '{database}'" if database else "MariaDB / MySQL"
218
+ console.print(f"[bold yellow]Importing '{sql_path.name}' ({sql_path.stat().st_size:,} bytes) into {target_desc}...[/bold yellow]")
219
+
220
+ try:
221
+ with open(sql_path, "r", encoding="utf-8", errors="ignore") as f:
222
+ res = subprocess.run(cmd, stdin=f, capture_output=True, text=True)
223
+ except Exception as e:
224
+ logger.error(f"Failed to read SQL file '{sql_path}': {e}")
225
+ raise typer.Exit(code=1)
226
+
227
+ if res.returncode != 0:
228
+ logger.error(f"MySQL Import Error:\n{res.stderr.strip()}")
229
+ raise typer.Exit(code=1)
230
+
231
+ console.print(f"[bold green]✓ Successfully imported '{sql_path.name}' into {target_desc}![/bold green]")
232
+
163
233
  def execute_create_user(host: str, port: int, user: str, password: Optional[str], new_user: str, new_pass: str, grant_db: Optional[str], user_host: str):
164
234
  logger.info(f"Creating user '{new_user}' on MySQL...")
165
235
  if not shutil.which("mysql"):
@@ -273,6 +343,49 @@ def export_db_cmd(
273
343
  triggers=triggers
274
344
  )
275
345
 
346
+ @db_app.command("import")
347
+ @db_app.command("import-db")
348
+ def import_db_cmd(
349
+ target: Optional[str] = typer.Argument(None, help="Path to .sql file OR database name"),
350
+ file_or_db: Optional[str] = typer.Argument(None, help="Path to .sql file if database name was specified first, or database name"),
351
+ database: Optional[str] = typer.Option(None, "--database", "-d", "--name", "-n", help="Target database name (optional if SQL dump contains CREATE DATABASE / USE)"),
352
+ host: str = typer.Option("localhost", "--host", "-h", help="Database host"),
353
+ port: int = typer.Option(3306, "--port", "-P", help="Database port"),
354
+ user: str = typer.Option("root", "--user", "-u", help="Admin username"),
355
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="Admin password (default: root)"),
356
+ ):
357
+ """Import a SQL dump file into MariaDB/MySQL."""
358
+ sql_file = None
359
+ target_db = database
360
+
361
+ if target:
362
+ p_target = Path(target).expanduser()
363
+ if p_target.exists() or target.endswith(".sql"):
364
+ sql_file = str(p_target)
365
+ if file_or_db and not target_db:
366
+ target_db = file_or_db
367
+ else:
368
+ target_db = target
369
+ if file_or_db:
370
+ sql_file = str(Path(file_or_db).expanduser())
371
+ elif file_or_db:
372
+ sql_file = str(Path(file_or_db).expanduser())
373
+
374
+ if not sql_file:
375
+ sql_file = typer.prompt("Path to .sql file").strip()
376
+ if not sql_file:
377
+ logger.error("SQL file path is required.")
378
+ raise typer.Exit(code=1)
379
+
380
+ execute_import_db(
381
+ host=host,
382
+ port=port,
383
+ user=user,
384
+ password=password,
385
+ sql_file=sql_file,
386
+ database=target_db,
387
+ )
388
+
276
389
  @db_app.command("create-user")
277
390
  def create_user_cmd(
278
391
  username: str = typer.Argument(None, help="Username"),
@@ -328,9 +441,65 @@ def configure_mariadb_root_password(new_password: str = "root", sudo_password: O
328
441
  from ndev.linux.runtime.nginx_helper import run_privileged_command
329
442
  from ndev.common.utils import which_binary
330
443
 
444
+ client_bin = which_binary("mariadb") or which_binary("mysql") or "mariadb"
445
+
446
+ # 1. Ensure MariaDB service is active
447
+ try:
448
+ run_privileged_command(["systemctl", "start", "mariadb"], password=sudo_password, check=False)
449
+ except Exception:
450
+ pass
451
+
452
+ # 2. Detect MariaDB unix socket path
453
+ socket_candidates = [
454
+ Path("/run/mysql/mysql.sock"),
455
+ Path("/run/mysqld/mysqld.sock"),
456
+ Path("/var/run/mysqld/mysqld.sock"),
457
+ Path("/var/lib/mysql/mysql.sock"),
458
+ ]
459
+ socket_args: list[str] = []
460
+ for s in socket_candidates:
461
+ if s.exists():
462
+ socket_args = ["-S", str(s)]
463
+ break
464
+
465
+ # 3. Dual auth (unix_socket + password) attempt for localhost
466
+ dual_auth_sql = (
467
+ f"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('{new_password}') OR unix_socket; "
468
+ "FLUSH PRIVILEGES;"
469
+ )
470
+ try:
471
+ run_privileged_command([client_bin] + socket_args + ["-u", "root", "-e", dual_auth_sql], password=sudo_password, check=False)
472
+ except Exception as e:
473
+ logger.debug(f"Dual auth attempt: {e}")
474
+
475
+ # 4. Standard root password & network user creation
476
+ batch_sql = (
477
+ f"ALTER USER 'root'@'localhost' IDENTIFIED BY '{new_password}'; "
478
+ f"CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '{new_password}'; "
479
+ f"ALTER USER 'root'@'127.0.0.1' IDENTIFIED BY '{new_password}'; "
480
+ f"CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '{new_password}'; "
481
+ f"ALTER USER 'root'@'%' IDENTIFIED BY '{new_password}'; "
482
+ "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION; "
483
+ "GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION; "
484
+ "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; "
485
+ "FLUSH PRIVILEGES;"
486
+ )
487
+
488
+ try:
489
+ run_privileged_command([client_bin] + socket_args + ["-u", "root", "-f", "-e", batch_sql], password=sudo_password, check=False)
490
+ except Exception as e:
491
+ logger.debug(f"Batch SQL config: {e}")
492
+
493
+ # Also try if root already has password set
494
+ try:
495
+ run_privileged_command([client_bin] + socket_args + ["-u", "root", f"--password={new_password}", "-f", "-e", batch_sql], password=sudo_password, check=False)
496
+ except Exception:
497
+ pass
498
+
499
+ # 5. Fallback individual statements
331
500
  sql_statements = [
332
- f"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('{new_password}') OR unix_socket;",
333
501
  f"ALTER USER 'root'@'localhost' IDENTIFIED BY '{new_password}';",
502
+ f"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{new_password}');",
334
503
  f"CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY '{new_password}';",
335
504
  f"ALTER USER 'root'@'127.0.0.1' IDENTIFIED BY '{new_password}';",
336
505
  f"CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY '{new_password}';",
@@ -341,16 +510,14 @@ def configure_mariadb_root_password(new_password: str = "root", sudo_password: O
341
510
  "FLUSH PRIVILEGES;",
342
511
  ]
343
512
 
344
- client_bin = which_binary("mariadb") or which_binary("mysql") or "mariadb"
345
-
346
513
  for sql in sql_statements:
347
514
  try:
348
- cmd = [client_bin, "-u", "root", "-e", sql]
515
+ cmd = [client_bin] + socket_args + ["-u", "root", "-e", sql]
349
516
  run_privileged_command(cmd, password=sudo_password, check=False)
350
517
  except Exception:
351
518
  pass
352
519
 
353
- # Verify if connection succeeds via TCP
520
+ # 6. Verify if connection succeeds via TCP
354
521
  for h in ["127.0.0.1", "localhost"]:
355
522
  try:
356
523
  test_cmd = [client_bin, "-h", h, "-P", "3306", "-u", "root", f"--password={new_password}", "-e", "SELECT 1;"]
@@ -369,6 +536,25 @@ def set_root_password_cmd(
369
536
  password: str = typer.Option("root", "--password", "-p", help="New root password (default: root)"),
370
537
  ):
371
538
  """Set or reset MariaDB root password and enable password login across localhost and 127.0.0.1."""
539
+ if os.geteuid() != 0:
540
+ console.print("[bold yellow]MariaDB root configuration requires administrator privileges. Elevating via sudo...[/bold yellow]")
541
+ cmd = [
542
+ "sudo",
543
+ sys.executable,
544
+ "-m",
545
+ "ndev",
546
+ "db",
547
+ "set-root-password",
548
+ "--password",
549
+ password,
550
+ ]
551
+ try:
552
+ res = subprocess.run(cmd)
553
+ raise typer.Exit(code=res.returncode)
554
+ except KeyboardInterrupt:
555
+ console.print("\n[red]Cancelled by user.[/red]")
556
+ raise typer.Exit(code=130)
557
+
372
558
  console.print(f"[bold yellow]Configuring MariaDB root access with password '{password}'...[/bold yellow]")
373
559
  ok = configure_mariadb_root_password(new_password=password)
374
560
  if ok:
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import os
9
9
  import re
10
+ import sys
10
11
  import socket
11
12
  import shutil
12
13
  import subprocess
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.15
3
+ Version: 0.3.17
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.15"
7
+ version = "0.3.17"
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"
@@ -559,6 +559,34 @@ class TestDatabaseCommands(unittest.TestCase):
559
559
  self.assertTrue(ok)
560
560
  mock_priv.assert_called()
561
561
 
562
+ def test_set_root_password_cmd_elevates_when_non_root(self):
563
+ import typer
564
+ from ndev.linux.commands.db import set_root_password_cmd
565
+ with patch("os.geteuid", return_value=1000):
566
+ with patch("subprocess.run") as mock_run:
567
+ mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0)
568
+ with self.assertRaises((SystemExit, typer.Exit)) as cm:
569
+ set_root_password_cmd(password="root")
570
+ exit_code = getattr(cm.exception, "exit_code", getattr(cm.exception, "code", 0))
571
+ self.assertEqual(exit_code, 0)
572
+ mock_run.assert_called_once()
573
+ self.assertIn("sudo", mock_run.call_args[0][0])
574
+ self.assertIn("set-root-password", mock_run.call_args[0][0])
575
+
576
+ def test_import_db_cmd(self):
577
+ import tempfile
578
+ from ndev.linux.commands.db import import_db_cmd
579
+ with tempfile.NamedTemporaryFile(suffix=".sql") as tf:
580
+ tf.write(b"SELECT 1;\n")
581
+ tf.flush()
582
+ with patch("subprocess.run") as mock_run:
583
+ mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0)
584
+ import_db_cmd(target=tf.name, database="testdb", user="root", password="root")
585
+ # Ensure subprocess.run was invoked with mysql/mariadb and the target database
586
+ cmd_args = mock_run.call_args[0][0]
587
+ self.assertIn("testdb", cmd_args)
588
+ self.assertIn("--password=root", cmd_args)
589
+
562
590
 
563
591
  class TestTuiEnsureSudo(unittest.TestCase):
564
592
  def test_ensure_sudo_when_not_needed(self):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes