ndev-stack 0.3.0__tar.gz → 0.3.1__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 (102) hide show
  1. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/PKG-INFO +2 -1
  2. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/README.md +1 -0
  3. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/__init__.py +1 -1
  4. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mongodb/module.py +8 -3
  5. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/postgres/module.py +8 -3
  6. ndev_stack-0.3.1/ndev/win/bin/shim.exe +0 -0
  7. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/cli.py +78 -4
  8. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/php.py +18 -28
  9. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/redis_core.py +8 -5
  10. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/setup.py +27 -27
  11. ndev_stack-0.3.1/ndev/win/core/shim.py +284 -0
  12. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/upgrade.py +12 -11
  13. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/PKG-INFO +2 -1
  14. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/SOURCES.txt +4 -1
  15. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/pyproject.toml +2 -2
  16. ndev_stack-0.3.1/tests/test_shim.py +86 -0
  17. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/__main__.py +0 -0
  18. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/cli.py +0 -0
  19. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/__init__.py +0 -0
  20. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/config.py +0 -0
  21. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/constants.py +0 -0
  22. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/github.py +0 -0
  23. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/logger.py +0 -0
  24. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/manifest.py +0 -0
  25. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/__init__.py +0 -0
  26. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/base.py +0 -0
  27. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
  28. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mailpit/module.py +0 -0
  29. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
  30. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
  31. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/redis/manifest.json +0 -0
  32. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/builtin/redis/module.py +0 -0
  33. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/modules/manager.py +0 -0
  34. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/common/utils.py +0 -0
  35. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/__init__.py +0 -0
  36. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/manager.py +0 -0
  37. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/packages.py +0 -0
  38. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/chroot/shell.py +0 -0
  39. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/cli.py +0 -0
  40. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/available.py +0 -0
  41. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/clean.py +0 -0
  42. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/ctl.py +0 -0
  43. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/current.py +0 -0
  44. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/db.py +0 -0
  45. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/doctor.py +0 -0
  46. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/grok.py +0 -0
  47. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/install.py +0 -0
  48. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/list.py +0 -0
  49. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/logs.py +0 -0
  50. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/mailpit.py +0 -0
  51. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/modules.py +0 -0
  52. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/mongodb.py +0 -0
  53. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/postgres.py +0 -0
  54. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/reload.py +0 -0
  55. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/restart.py +0 -0
  56. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/setup.py +0 -0
  57. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/start.py +0 -0
  58. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/status.py +0 -0
  59. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/stop.py +0 -0
  60. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/uninstall.py +0 -0
  61. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/update.py +0 -0
  62. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/upgrade.py +0 -0
  63. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/use.py +0 -0
  64. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/commands/vhost.py +0 -0
  65. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/builder.py +0 -0
  66. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/downloader.py +0 -0
  67. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/extensions.py +0 -0
  68. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/installer.py +0 -0
  69. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/resolver.py +0 -0
  70. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/php/templates.py +0 -0
  71. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/fpm.py +0 -0
  72. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/mailpit.py +0 -0
  73. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/pma.py +0 -0
  74. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/process.py +0 -0
  75. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/sockets.py +0 -0
  76. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/runtime/upgrade.py +0 -0
  77. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/linux/tui.py +0 -0
  78. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/main.py +0 -0
  79. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/tui.py +0 -0
  80. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/__init__.py +0 -0
  81. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/commands/__init__.py +0 -0
  82. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/__init__.py +0 -0
  83. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/db.py +0 -0
  84. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/elevate.py +0 -0
  85. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/ext.py +0 -0
  86. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/fcgi.py +0 -0
  87. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/grok.py +0 -0
  88. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/logs.py +0 -0
  89. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/mailpit.py +0 -0
  90. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/mkcert.py +0 -0
  91. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/paths.py +0 -0
  92. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/pma.py +0 -0
  93. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/services.py +0 -0
  94. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/core/vhost.py +0 -0
  95. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/templates/vhost.conf.tmpl +0 -0
  96. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
  97. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev/win/tui.py +0 -0
  98. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/dependency_links.txt +0 -0
  99. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/entry_points.txt +0 -0
  100. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/requires.txt +0 -0
  101. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/ndev_stack.egg-info/top_level.txt +0 -0
  102. {ndev_stack-0.3.0 → ndev_stack-0.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -244,6 +244,7 @@ ndev ui
244
244
  | `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
245
245
  | `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
246
246
  | `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
247
+ | `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
247
248
  | `ndev doctor` | Run environment diagnostics and health checks |
248
249
  | `ndev logs` | View and tail service and virtual host logs |
249
250
  | `ndev clean` | Clean up downloads cache and stale runtime state |
@@ -223,6 +223,7 @@ ndev ui
223
223
  | `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
224
224
  | `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
225
225
  | `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
226
+ | `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
226
227
  | `ndev doctor` | Run environment diagnostics and health checks |
227
228
  | `ndev logs` | View and tail service and virtual host logs |
228
229
  | `ndev clean` | Clean up downloads cache and stale runtime state |
@@ -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.0"
4
+ __version__ = "0.3.1"
5
5
 
6
6
  from . import common as common
7
7
  from .common.logger import logger as logger
@@ -119,16 +119,21 @@ class Module:
119
119
  except Exception:
120
120
  pass
121
121
 
122
- # Copy tools to ~/.ndev/shims/
122
+ # Create tool shims in ~/.ndev/shims/
123
123
  shim_dir = self.module_dir.parent.parent / "shims"
124
124
  if shim_dir.exists():
125
125
  for tool in ["mongod.exe", "mongos.exe", "mongosh.exe"]:
126
126
  src_tool = self.bin_dir / tool
127
127
  if src_tool.exists():
128
+ name = tool[:-4] if tool.lower().endswith(".exe") else tool
128
129
  try:
129
- shutil.copy2(src_tool, shim_dir / tool)
130
+ from ndev.win.core import shim as win_shim
131
+ win_shim.create(name, str(src_tool), path_prepend=str(self.bin_dir), shim_dir=shim_dir)
130
132
  except Exception:
131
- pass
133
+ try:
134
+ shutil.copy2(src_tool, shim_dir / tool)
135
+ except Exception:
136
+ pass
132
137
  else:
133
138
  # Linux: try installing via system package manager or apt
134
139
  if not shutil.which("mongod"):
@@ -108,16 +108,21 @@ class Module:
108
108
  except Exception:
109
109
  pass
110
110
 
111
- # Copy key cli binaries to ~/.ndev/shims/
111
+ # Create key cli binary shims in ~/.ndev/shims/
112
112
  shim_dir = self.module_dir.parent.parent / "shims"
113
113
  if shim_dir.exists():
114
114
  for tool in ["psql.exe", "pg_ctl.exe", "pg_dump.exe", "createdb.exe", "dropdb.exe"]:
115
115
  src_tool = self.bin_dir / tool
116
116
  if src_tool.exists():
117
+ name = tool[:-4] if tool.lower().endswith(".exe") else tool
117
118
  try:
118
- shutil.copy2(src_tool, shim_dir / tool)
119
+ from ndev.win.core import shim as win_shim
120
+ win_shim.create(name, str(src_tool), path_prepend=str(self.bin_dir), shim_dir=shim_dir)
119
121
  except Exception:
120
- pass
122
+ try:
123
+ shutil.copy2(src_tool, shim_dir / tool)
124
+ except Exception:
125
+ pass
121
126
  else:
122
127
  # Linux: try installing via system package manager
123
128
  if not shutil.which("pg_ctl"):
Binary file
@@ -33,6 +33,7 @@ from .core import (
33
33
  redis_core,
34
34
  services,
35
35
  setup as setup_core,
36
+ shim,
36
37
  upgrade as upgrade_core,
37
38
  vhost as vhost_core,
38
39
  )
@@ -375,8 +376,8 @@ def doctor():
375
376
  table.add_row("ngrok Tunneling", "[yellow]MISSING[/yellow]", "Run `ndev setup` to install")
376
377
 
377
378
  # Composer
378
- composer_bat = paths.SHIM_DIR / "composer.bat"
379
- if composer_bat.exists() or shutil.which("composer"):
379
+ composer_cmd = paths.SHIM_DIR / "composer.exe"
380
+ if composer_cmd.exists() or shutil.which("composer"):
380
381
  table.add_row("Composer", "[green]OK[/green]", "Installed and available on CLI")
381
382
  else:
382
383
  table.add_row("Composer", "[yellow]MISSING[/yellow]", "Run `ndev setup` to install")
@@ -390,7 +391,7 @@ def doctor():
390
391
  # PATH Check
391
392
  shim_on_path = str(paths.SHIM_DIR).lower() in os.environ.get("PATH", "").lower()
392
393
  path_status = "[green]OK[/green]" if shim_on_path else "[yellow]WARNING[/yellow]"
393
- path_detail = f"Found on PATH ({paths.SHIM_DIR})" if shim_on_path else f"Add {paths.SHIM_DIR} to your system PATH"
394
+ path_detail = f"Found on PATH ({paths.SHIM_DIR})" if shim_on_path else f"Run `ndev shim init` or add {paths.SHIM_DIR} to PATH"
394
395
  table.add_row("Shims on PATH", path_status, path_detail)
395
396
 
396
397
  console.print(table)
@@ -1994,9 +1995,10 @@ def setup(nginx, mariadb, mkcert, ngrok, composer, cacert, nginx_version, mariad
1994
1995
  for name, path in results.items():
1995
1996
  console.print(f"[bold green]✓[/bold green] Installed {name:<10} -> {path}")
1996
1997
 
1998
+ shim.ensure_on_path()
1997
1999
  shim_on_path = str(paths.SHIM_DIR).lower() in os.environ.get("PATH", "").lower()
1998
2000
  if not shim_on_path:
1999
- console.print(f"\n[bold yellow]Important:[/bold yellow] Add [bold cyan]{paths.SHIM_DIR}[/bold cyan] to your system PATH.")
2001
+ console.print(f"\n[bold green]✓[/bold green] Added [bold cyan]{paths.SHIM_DIR}[/bold cyan] to User PATH (open a new terminal to use shims).")
2000
2002
 
2001
2003
 
2002
2004
  # ---- Component Upgrade (upgrade) --------------------------------------------
@@ -2372,6 +2374,78 @@ def mongodb_status_cmd():
2372
2374
  console.print(f"MongoDB: {'[bold green]RUNNING[/bold green]' if st.get('running') else '[bold red]STOPPED[/bold red]'} ({st.get('details', '')})")
2373
2375
 
2374
2376
 
2377
+ # ---- Shim Management (shim / shims) -----------------------------------------
2378
+
2379
+ @main.group(name="shim")
2380
+ def shim_cmd():
2381
+ """Manage Windows executable shims and PATH registration."""
2382
+ pass
2383
+
2384
+
2385
+ @shim_cmd.command(name="list")
2386
+ def shim_list_cmd():
2387
+ """List all configured shims."""
2388
+ shims = shim.list_shims()
2389
+ if not shims:
2390
+ console.print("[yellow]No shims configured in ~/.ndev/shims.[/yellow]")
2391
+ return
2392
+
2393
+ table = Table(title="ndev Windows Shims")
2394
+ table.add_column("Command", style="bold cyan")
2395
+ table.add_column("Target Executable / Details", style="green")
2396
+ for name, target in shims.items():
2397
+ table.add_row(name, target)
2398
+ console.print(table)
2399
+
2400
+
2401
+ @shim_cmd.command(name="init")
2402
+ def shim_init_cmd():
2403
+ """Ensure ~/.ndev/shims is added to the user PATH environment variable."""
2404
+ updated = shim.ensure_on_path()
2405
+ if updated:
2406
+ console.print(f"[bold green]✓ Added {paths.SHIM_DIR} to User PATH registry.[/bold green]")
2407
+ console.print("[dim]A WM_SETTINGCHANGE broadcast was sent. Open a new terminal to use shims.[/dim]")
2408
+ else:
2409
+ console.print(f"[green]✓ {paths.SHIM_DIR} is already present in your User PATH registry.[/green]")
2410
+
2411
+
2412
+ @shim_cmd.command(name="add")
2413
+ @click.argument("name")
2414
+ @click.argument("target")
2415
+ @click.option("--args", default="", help="Default arguments to pass to target executable")
2416
+ @click.option("--cwd", default=None, help="Working directory for execution")
2417
+ @click.option("--prepend", default=None, help="Directory to prepend to PATH for target")
2418
+ def shim_add_cmd(name, target, args, cwd, prepend):
2419
+ """Create or update a custom native Windows .exe shim."""
2420
+ try:
2421
+ created = shim.create(name, target, args=args, cwd=cwd, path_prepend=prepend)
2422
+ console.print(f"[bold green]✓ Created shim '{name}' -> {created}[/bold green]")
2423
+ except Exception as e:
2424
+ raise click.ClickException(str(e))
2425
+
2426
+
2427
+ @shim_cmd.command(name="rm")
2428
+ @click.argument("name")
2429
+ def shim_rm_cmd(name):
2430
+ """Remove a shim."""
2431
+ if shim.remove(name):
2432
+ console.print(f"[bold green]✓ Removed shim '{name}'.[/bold green]")
2433
+ else:
2434
+ console.print(f"[yellow]No shim named '{name}' found.[/yellow]")
2435
+
2436
+
2437
+ @main.group(name="shims", hidden=True)
2438
+ def shims_alias():
2439
+ """Alias for shim."""
2440
+ pass
2441
+
2442
+
2443
+ shims_alias.add_command(shim_list_cmd, "list")
2444
+ shims_alias.add_command(shim_init_cmd, "init")
2445
+ shims_alias.add_command(shim_add_cmd, "add")
2446
+ shims_alias.add_command(shim_rm_cmd, "rm")
2447
+
2448
+
2375
2449
  if __name__ == "__main__":
2376
2450
  sys.exit(main())
2377
2451
 
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
 
16
16
  import httpx
17
17
 
18
- from . import paths
18
+ from . import paths, shim
19
19
 
20
20
  RELEASES_INDEX_URL = "https://windows.php.net/downloads/releases/releases.json"
21
21
  ARCHIVES_INDEX_URL = "https://windows.php.net/downloads/releases/archives/"
@@ -498,12 +498,9 @@ def uninstall(version: str) -> None:
498
498
  if paths.CURRENT_FILE.exists():
499
499
  paths.CURRENT_FILE.unlink(missing_ok=True)
500
500
  # Remove all PHP and Composer shims
501
- for shim in [
502
- "php.bat", "php.cmd", "php.ps1",
503
- "php-cgi.bat", "php-cgi.cmd", "php-cgi.ps1",
504
- "composer.bat", "composer.cmd", "composer.ps1",
505
- ]:
506
- (paths.SHIM_DIR / shim).unlink(missing_ok=True)
501
+ shim.remove("php")
502
+ shim.remove("php-cgi")
503
+ shim.remove("composer")
507
504
 
508
505
 
509
506
  def list_installed() -> list[str]:
@@ -570,37 +567,30 @@ def get_current_version() -> str | None:
570
567
  def use(version: str) -> str:
571
568
  """
572
569
  Point ndev shims (on PATH) at this version's php.exe and php-cgi.exe.
573
- Generates php.bat, php.cmd, php-cgi.bat, and composer.bat in ~/.ndev/shims.
570
+ Generates binary .exe shims with .cmd fallback in ~/.ndev/shims.
574
571
  Returns the resolved version string.
575
572
  """
576
573
  resolved_ver = resolve_installed(version)
577
574
  paths.ensure_dirs()
578
575
 
579
- exe_path = str(paths.version_dir(resolved_ver) / "php.exe")
580
- cgi_path = str(paths.version_dir(resolved_ver) / "php-cgi.exe")
576
+ php_dir = paths.version_dir(resolved_ver)
577
+ exe_path = str(php_dir / "php.exe")
578
+ cgi_path = str(php_dir / "php-cgi.exe")
581
579
 
582
- # php.bat, php.cmd, php.ps1
583
- (paths.SHIM_DIR / "php.bat").write_text(f'@echo off\r\n"{exe_path}" %*\r\n', encoding="utf-8")
584
- (paths.SHIM_DIR / "php.cmd").write_text(f'@echo off\r\n"{exe_path}" %*\r\n', encoding="utf-8")
585
- (paths.SHIM_DIR / "php.ps1").write_text(f'& "{exe_path}" @args\r\n', encoding="utf-8")
580
+ # Create native shims via shimgen with path_prepend for PHP DLLs (cleans any legacy scripts)
581
+ shim.create("php", exe_path, path_prepend=str(php_dir), clean_legacy=True)
582
+ shim.create("php-cgi", cgi_path, path_prepend=str(php_dir), clean_legacy=True)
586
583
 
587
- # php-cgi.bat, php-cgi.cmd, php-cgi.ps1
588
- (paths.SHIM_DIR / "php-cgi.bat").write_text(f'@echo off\r\n"{cgi_path}" %*\r\n', encoding="utf-8")
589
- (paths.SHIM_DIR / "php-cgi.cmd").write_text(f'@echo off\r\n"{cgi_path}" %*\r\n', encoding="utf-8")
590
- (paths.SHIM_DIR / "php-cgi.ps1").write_text(f'& "{cgi_path}" @args\r\n', encoding="utf-8")
591
-
592
- # If composer.phar is present, create/update composer shims
584
+ # If composer.phar is present, create/update composer native shim
593
585
  composer_phar = paths.SHIM_DIR / "composer.phar"
594
586
  if composer_phar.exists():
595
587
  clean_phar = str(composer_phar.resolve()).replace("\\", "/")
596
- (paths.SHIM_DIR / "composer.bat").write_text(
597
- f'@echo off\r\n"{exe_path}" "{clean_phar}" %*\r\n', encoding="utf-8"
598
- )
599
- (paths.SHIM_DIR / "composer.cmd").write_text(
600
- f'@echo off\r\n"{exe_path}" "{clean_phar}" %*\r\n', encoding="utf-8"
601
- )
602
- (paths.SHIM_DIR / "composer.ps1").write_text(
603
- f'& "{exe_path}" "{clean_phar}" @args\r\n', encoding="utf-8"
588
+ shim.create(
589
+ "composer",
590
+ exe_path,
591
+ args=f'"{clean_phar}"',
592
+ path_prepend=str(php_dir),
593
+ clean_legacy=True,
604
594
  )
605
595
 
606
596
  paths.set_current_version(resolved_ver)
@@ -16,7 +16,7 @@ import time
16
16
  import zipfile
17
17
  from pathlib import Path
18
18
 
19
- from . import fcgi, paths, setup as setup_core
19
+ from . import fcgi, paths, setup as setup_core, shim
20
20
 
21
21
  DEFAULT_PORT = 6379
22
22
  DEFAULT_VERSION = "8.10.1"
@@ -103,12 +103,15 @@ def install(version: str = DEFAULT_VERSION) -> Path:
103
103
  )
104
104
 
105
105
  # Place redis-cli shim in ~/.ndev/shims/
106
- if (REDIS_DIR / "redis-cli.exe").exists():
107
- shim_cli = paths.SHIM_DIR / "redis-cli.exe"
106
+ cli_file = REDIS_DIR / "redis-cli.exe"
107
+ if cli_file.exists():
108
108
  try:
109
- shutil.copy2(REDIS_DIR / "redis-cli.exe", shim_cli)
109
+ shim.create("redis-cli", str(cli_file), path_prepend=str(REDIS_DIR))
110
110
  except Exception:
111
- pass
111
+ try:
112
+ shutil.copy2(cli_file, paths.SHIM_DIR / "redis-cli.exe")
113
+ except Exception:
114
+ pass
112
115
 
113
116
  return REDIS_DIR
114
117
 
@@ -12,7 +12,7 @@ from typing import Optional
12
12
 
13
13
  import httpx
14
14
 
15
- from . import paths
15
+ from . import paths, shim
16
16
 
17
17
  # ---- Defaults ---------------------------------------------------------------
18
18
 
@@ -170,16 +170,13 @@ def install_mariadb(version: str = DEFAULT_MARIADB_VERSION) -> Path:
170
170
 
171
171
  def _create_mariadb_shims() -> None:
172
172
  paths.ensure_dirs()
173
- mysql_exe = paths.MARIADB_DIR / "bin" / "mysql.exe"
174
- mysqldump_exe = paths.MARIADB_DIR / "bin" / "mysqldump.exe"
173
+ bin_dir = paths.MARIADB_DIR / "bin"
174
+ mysql_exe = bin_dir / "mysql.exe"
175
+ mysqldump_exe = bin_dir / "mysqldump.exe"
175
176
  if mysql_exe.exists():
176
- (paths.SHIM_DIR / "mysql.bat").write_text(f'@echo off\r\n"{mysql_exe}" %*\r\n', encoding="utf-8")
177
- (paths.SHIM_DIR / "mysql.cmd").write_text(f'@echo off\r\n"{mysql_exe}" %*\r\n', encoding="utf-8")
178
- (paths.SHIM_DIR / "mysql.ps1").write_text(f'& "{mysql_exe}" @args\r\n', encoding="utf-8")
177
+ shim.create("mysql", str(mysql_exe), path_prepend=str(bin_dir), clean_legacy=True)
179
178
  if mysqldump_exe.exists():
180
- (paths.SHIM_DIR / "mysqldump.bat").write_text(f'@echo off\r\n"{mysqldump_exe}" %*\r\n', encoding="utf-8")
181
- (paths.SHIM_DIR / "mysqldump.cmd").write_text(f'@echo off\r\n"{mysqldump_exe}" %*\r\n', encoding="utf-8")
182
- (paths.SHIM_DIR / "mysqldump.ps1").write_text(f'& "{mysqldump_exe}" @args\r\n', encoding="utf-8")
179
+ shim.create("mysqldump", str(mysqldump_exe), path_prepend=str(bin_dir), clean_legacy=True)
183
180
 
184
181
 
185
182
  def _init_mariadb_data_dir() -> None:
@@ -280,14 +277,28 @@ def install_ngrok() -> Path:
280
277
  # ---- Composer -----------------------------------------------------------------
281
278
 
282
279
  def install_composer() -> Path:
283
- """Download composer.phar and generate composer.bat, composer.cmd, and composer.ps1 shims."""
280
+ """Download composer.phar and generate native composer.exe shim."""
284
281
  phar_dest = paths.SHIM_DIR / "composer.phar"
285
282
  _download(COMPOSER_PHAR_URL, phar_dest)
286
283
 
287
284
  clean_phar = str(phar_dest.resolve()).replace("\\", "/")
288
- (paths.SHIM_DIR / "composer.bat").write_text(f'@echo off\r\nphp "{clean_phar}" %*\r\n', encoding="utf-8")
289
- (paths.SHIM_DIR / "composer.cmd").write_text(f'@echo off\r\nphp "{clean_phar}" %*\r\n', encoding="utf-8")
290
- (paths.SHIM_DIR / "composer.ps1").write_text(f'& php "{clean_phar}" @args\r\n', encoding="utf-8")
285
+ # If active PHP is installed, point composer shim directly to it
286
+ from . import php
287
+ curr_ver = php.get_current_version()
288
+ if curr_ver:
289
+ try:
290
+ exe_path = str(paths.version_dir(curr_ver) / "php.exe")
291
+ shim.create(
292
+ "composer",
293
+ exe_path,
294
+ args=f'"{clean_phar}"',
295
+ path_prepend=str(paths.version_dir(curr_ver)),
296
+ clean_legacy=True,
297
+ )
298
+ return phar_dest
299
+ except Exception:
300
+ pass
301
+
291
302
  return phar_dest
292
303
 
293
304
 
@@ -306,25 +317,14 @@ def install_cacert() -> Path:
306
317
 
307
318
 
308
319
  def create_ndev_shims() -> Path:
309
- """Create ndev.bat, ndev.cmd, and ndev.ps1 in ~/.ndev/shims/ pointing to active python/ndev."""
320
+ """Create ndev native executable shim in ~/.ndev/shims/ pointing to active python/ndev."""
310
321
  import sys
311
322
  paths.ensure_dirs()
312
323
  scripts_dir = Path(sys.executable).parent
313
324
  ndev_exe = scripts_dir / "ndev.exe"
314
325
  if ndev_exe.exists():
315
- bat_target = f'"{ndev_exe}" %*'
316
- ps_target = f'& "{ndev_exe}" @args'
317
- else:
318
- bat_target = f'"{sys.executable}" -m ndev_win.cli %*'
319
- ps_target = f'& "{sys.executable}" -m ndev_win.cli @args'
320
-
321
- bat_path = paths.SHIM_DIR / "ndev.bat"
322
- cmd_path = paths.SHIM_DIR / "ndev.cmd"
323
- ps_path = paths.SHIM_DIR / "ndev.ps1"
324
- bat_path.write_text(f'@echo off\r\n{bat_target}\r\n', encoding="utf-8")
325
- cmd_path.write_text(f'@echo off\r\n{bat_target}\r\n', encoding="utf-8")
326
- ps_path.write_text(f'{ps_target}\r\n', encoding="utf-8")
327
- return bat_path
326
+ return shim.create("ndev", str(ndev_exe), clean_legacy=True)
327
+ return shim.create("ndev", sys.executable, args="-m ndev.main", clean_legacy=True)
328
328
 
329
329
 
330
330
  # ---- Orchestration ----------------------------------------------------------
@@ -0,0 +1,284 @@
1
+ """
2
+ shim generator and manager for ndev on Windows.
3
+
4
+ Provides native Windows .exe shims using shim.exe + <name>.shim config pairs.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import filecmp
9
+ import ntpath
10
+ import os
11
+ import re
12
+ import shutil
13
+ import sys
14
+ import warnings
15
+ from pathlib import Path
16
+
17
+ from . import paths
18
+
19
+ # By default, use ndev's central SHIM_DIR
20
+ SHIM_DIR = paths.SHIM_DIR
21
+
22
+
23
+ def _find_shim_exe() -> Path:
24
+ """Find prebuilt shim.exe binary across installed package or repository."""
25
+ # 1. Check explicit environment override
26
+ env_exe = os.environ.get("NDEV_SHIM_EXE")
27
+ if env_exe:
28
+ p = Path(env_exe)
29
+ if p.exists():
30
+ return p
31
+
32
+ # 2. Check bundled package binary (ndev/win/bin/shim.exe)
33
+ pkg_exe = Path(__file__).parent.parent / "bin" / "shim.exe"
34
+ if pkg_exe.exists():
35
+ return pkg_exe
36
+
37
+ # 3. Check development repository root (shimgen/shim.exe)
38
+ repo_exe = Path(__file__).resolve().parent.parent.parent.parent / "shimgen" / "shim.exe"
39
+ if repo_exe.exists():
40
+ return repo_exe
41
+
42
+ return pkg_exe
43
+
44
+
45
+ SHIM_EXE = _find_shim_exe()
46
+
47
+ _NAME_RE = re.compile(r"^[\w.+\-]+$")
48
+
49
+ _RESERVED = {
50
+ "con", "prn", "aux", "nul",
51
+ *(f"com{i}" for i in range(1, 10)),
52
+ *(f"lpt{i}" for i in range(1, 10)),
53
+ }
54
+
55
+
56
+ def _check_name(name: str) -> str:
57
+ if (
58
+ not _NAME_RE.match(name)
59
+ or name.endswith(".")
60
+ or name.split(".")[0].lower() in _RESERVED
61
+ ):
62
+ raise ValueError(f"invalid shim name: {name!r}")
63
+ return name
64
+
65
+
66
+ def _write(path: Path, text: str, encoding: str = "utf-8") -> None:
67
+ """Atomic write: a shim starting mid-update never sees a half-written file."""
68
+ tmp = path.with_name(f"{path.name}.tmp-{os.getpid()}")
69
+ with open(tmp, "w", encoding=encoding, newline="\r\n") as f:
70
+ f.write(text)
71
+ try:
72
+ os.replace(tmp, path)
73
+ except OSError:
74
+ # Fallback if target is momentarily locked
75
+ try:
76
+ if path.exists():
77
+ _unlink(path)
78
+ os.replace(tmp, path)
79
+ except OSError:
80
+ try:
81
+ tmp.unlink(missing_ok=True)
82
+ except OSError:
83
+ pass
84
+ raise
85
+
86
+
87
+ def _unlink(p: Path) -> bool:
88
+ """Delete p. A running .exe can't be deleted on Windows but can be renamed,
89
+ so rename it out of the way and sweep it later."""
90
+ if not p.exists():
91
+ return False
92
+ try:
93
+ p.unlink()
94
+ except PermissionError:
95
+ try:
96
+ os.replace(p, p.with_name(f"{p.name}.old-{os.getpid()}"))
97
+ except OSError:
98
+ pass
99
+ except OSError:
100
+ return False
101
+ return True
102
+
103
+
104
+ def _sweep(shim_dir: Path) -> None:
105
+ for p in shim_dir.glob("*.old-*"):
106
+ try:
107
+ p.unlink()
108
+ except OSError:
109
+ pass # still running; sweep next time
110
+
111
+
112
+ def _same(a: Path, b: Path) -> bool:
113
+ try:
114
+ return os.path.samefile(a, b) or filecmp.cmp(a, b, shallow=False)
115
+ except OSError:
116
+ return False
117
+
118
+
119
+ def _install_exe(dst: Path) -> None:
120
+ if dst.exists():
121
+ if _same(SHIM_EXE, dst):
122
+ return # already current (also avoids touching an exe that is running)
123
+ try:
124
+ os.replace(dst, dst.with_name(f"{dst.name}.old-{os.getpid()}"))
125
+ except OSError:
126
+ pass
127
+ try:
128
+ os.link(SHIM_EXE, dst) # hard link: fast, zero disk copy overhead
129
+ except OSError:
130
+ shutil.copyfile(SHIM_EXE, dst) # cross-volume or non-NTFS fallback
131
+
132
+
133
+ def create(
134
+ name: str,
135
+ target: str | Path,
136
+ args: str = "",
137
+ cwd: str | Path | None = None,
138
+ env: dict[str, str] | None = None,
139
+ path_prepend: str | Path | None = None,
140
+ shim_dir: Path | None = None,
141
+ clean_legacy: bool = True,
142
+ ) -> Path:
143
+ """Create or update a native Windows .exe shim in shim_dir (defaults to ~/.ndev/shims).
144
+
145
+ Returns the path to the generated <shim_dir>/<name>.exe.
146
+ """
147
+ _check_name(name)
148
+ shim_dir = Path(shim_dir or SHIM_DIR)
149
+ shim_dir.mkdir(parents=True, exist_ok=True)
150
+ target = str(target)
151
+ env = env or {}
152
+
153
+ if not (ntpath.isabs(target) or target.startswith("%")):
154
+ raise ValueError(f"shim target must be an absolute Windows path, got {target!r}")
155
+ if not os.path.isfile(os.path.expandvars(target)):
156
+ warnings.warn(f"shim target does not exist (yet): {target}", stacklevel=2)
157
+
158
+ _sweep(shim_dir)
159
+
160
+ if not SHIM_EXE.is_file():
161
+ raise FileNotFoundError(f"shim.exe not found at {SHIM_EXE}; build or place shim.exe first")
162
+
163
+ lines = [f"path = {target}"]
164
+ if args:
165
+ lines.append(f"args = {args}")
166
+ if cwd:
167
+ lines.append(f"cwd = {cwd}")
168
+ if path_prepend:
169
+ lines.append(f"path_prepend = {path_prepend}")
170
+ lines += [f"env.{k} = {v}" for k, v in sorted(env.items())]
171
+
172
+ # Write config first, exe second: the exe never exists without its config
173
+ _write(shim_dir / f"{name}.shim", "\n".join(lines) + "\n")
174
+ _install_exe(shim_dir / f"{name}.exe")
175
+
176
+ # Clean legacy script shims (.cmd, .bat, .ps1)
177
+ if clean_legacy:
178
+ _unlink(shim_dir / f"{name}.cmd")
179
+ _unlink(shim_dir / f"{name}.bat")
180
+ _unlink(shim_dir / f"{name}.ps1")
181
+
182
+ return shim_dir / f"{name}.exe"
183
+
184
+
185
+ def remove(name: str, shim_dir: Path | None = None) -> bool:
186
+ """Remove all shims and configs for `name` (.exe, .shim, .cmd, .bat, .ps1)."""
187
+ _check_name(name)
188
+ shim_dir = Path(shim_dir or SHIM_DIR)
189
+ removed = False
190
+ for ext in (".exe", ".shim", ".cmd", ".bat", ".ps1"):
191
+ removed |= _unlink(shim_dir / f"{name}{ext}")
192
+ return removed
193
+
194
+
195
+ def read_shim(name: str, shim_dir: Path | None = None) -> dict[str, str]:
196
+ """Read a .shim config file into a dictionary."""
197
+ shim_dir = Path(shim_dir or SHIM_DIR)
198
+ cfg: dict[str, str] = {}
199
+ p = shim_dir / f"{name}.shim"
200
+ if p.exists():
201
+ try:
202
+ for line in p.read_text(encoding="utf-8-sig").splitlines():
203
+ line = line.strip()
204
+ if line and not line.startswith("#") and "=" in line:
205
+ k, v = line.split("=", 1)
206
+ cfg[k.strip()] = v.strip()
207
+ except OSError:
208
+ pass
209
+ return cfg
210
+
211
+
212
+ def list_shims(shim_dir: Path | None = None) -> dict[str, str]:
213
+ """Return dict of {name: target_path_or_description} for all native shims."""
214
+ shim_dir = Path(shim_dir or SHIM_DIR)
215
+ if not shim_dir.is_dir():
216
+ return {}
217
+ out: dict[str, str] = {}
218
+
219
+ for p in sorted(shim_dir.iterdir()):
220
+ if p.suffix.lower() == ".exe" and (shim_dir / f"{p.stem}.shim").exists():
221
+ cfg = read_shim(p.stem, shim_dir)
222
+ t = cfg.get("path", "?")
223
+ out[p.stem] = (
224
+ t
225
+ if os.path.isfile(os.path.expandvars(t)) or sys.platform != "win32"
226
+ else f"{t} [MISSING]"
227
+ )
228
+
229
+ return out
230
+
231
+
232
+ def ensure_on_path(shim_dir: Path | None = None) -> bool:
233
+ """Add the shim directory to the HKCU user PATH registry if not already present.
234
+ Normalizes slashes/case, skips duplicate additions, and broadcasts WM_SETTINGCHANGE.
235
+ Returns True if user PATH was updated, False if already present.
236
+ """
237
+ if sys.platform != "win32":
238
+ return False
239
+
240
+ import ctypes
241
+ import winreg
242
+ from ctypes import wintypes
243
+
244
+ shim_dir_str = str(Path(shim_dir or SHIM_DIR).resolve())
245
+
246
+ def norm(p: str) -> str:
247
+ return os.path.normcase(os.path.normpath(os.path.expandvars(p.strip().rstrip("\\/"))))
248
+
249
+ norm_target = norm(shim_dir_str)
250
+
251
+ with winreg.OpenKey(
252
+ winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE
253
+ ) as key:
254
+ try:
255
+ cur, typ = winreg.QueryValueEx(key, "Path")
256
+ except FileNotFoundError:
257
+ cur, typ = "", winreg.REG_EXPAND_SZ
258
+
259
+ parts = [p.strip() for p in cur.split(";") if p.strip()]
260
+ if any(norm(p) == norm_target for p in parts):
261
+ return False
262
+
263
+ # Prepend to front so ndev shims take precedence
264
+ parts.insert(0, shim_dir_str)
265
+ winreg.SetValueEx(key, "Path", 0, typ, ";".join(parts))
266
+
267
+ try:
268
+ send = ctypes.windll.user32.SendMessageTimeoutW
269
+ send.argtypes = [
270
+ wintypes.HWND,
271
+ wintypes.UINT,
272
+ wintypes.WPARAM,
273
+ wintypes.LPCWSTR,
274
+ wintypes.UINT,
275
+ wintypes.UINT,
276
+ ctypes.POINTER(wintypes.DWORD),
277
+ ]
278
+ send.restype = wintypes.LPARAM
279
+ result = wintypes.DWORD()
280
+ send(0xFFFF, 0x001A, 0, "Environment", 2, 5000, ctypes.byref(result)) # HWND_BROADCAST, WM_SETTINGCHANGE
281
+ except Exception:
282
+ pass
283
+
284
+ return True
@@ -433,14 +433,15 @@ def get_composer_info() -> ComponentInfo:
433
433
 
434
434
  if installed:
435
435
  try:
436
- bat = paths.SHIM_DIR / "composer.bat"
437
- res = subprocess.run([str(bat), "--version", "--no-ansi"], capture_output=True, text=True, timeout=10)
438
- raw = (res.stdout or "") + "\n" + (res.stderr or "")
439
- # Strip ANSI escape sequences if any exist
440
- clean_text = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", raw)
441
- m = re.search(r"Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)", clean_text, re.IGNORECASE)
442
- if m:
443
- curr_ver = m.group(1)
436
+ comp_cmd = paths.SHIM_DIR / "composer.exe"
437
+ if comp_cmd.exists():
438
+ res = subprocess.run([str(comp_cmd), "--version", "--no-ansi"], capture_output=True, text=True, timeout=10)
439
+ raw = (res.stdout or "") + "\n" + (res.stderr or "")
440
+ # Strip ANSI escape sequences if any exist
441
+ clean_text = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", raw)
442
+ m = re.search(r"Composer\s+(?:version\s+)?(\d+\.\d+\.\d+)", clean_text, re.IGNORECASE)
443
+ if m:
444
+ curr_ver = m.group(1)
444
445
  except Exception as e:
445
446
  err = str(e)
446
447
 
@@ -469,9 +470,9 @@ def get_composer_info() -> ComponentInfo:
469
470
 
470
471
  def upgrade_composer() -> tuple[bool, str]:
471
472
  try:
472
- bat = paths.SHIM_DIR / "composer.bat"
473
- if bat.exists():
474
- res = subprocess.run([str(bat), "self-update"], capture_output=True, text=True, timeout=60)
473
+ comp_cmd = paths.SHIM_DIR / "composer.exe"
474
+ if comp_cmd.exists():
475
+ res = subprocess.run([str(comp_cmd), "self-update"], capture_output=True, text=True, timeout=60)
475
476
  if res.returncode == 0:
476
477
  info = get_composer_info()
477
478
  return True, f"Composer upgraded successfully ({info.current_version})."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ndev-stack
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -244,6 +244,7 @@ ndev ui
244
244
  | `ndev upgrade` | Check for and upgrade stack components (Nginx, MariaDB, Redis, PMA, Mailpit, mkcert, Composer) |
245
245
  | `ndev upgrade --check` | Check component versions against upstream releases without upgrading |
246
246
  | `ndev shell` | Interactive developer subshell pre-loaded with PHP/Composer/MySQL/Redis on PATH |
247
+ | `ndev shim` | Manage Windows binary shims and PATH registration (`list`, `init`, `add`, `rm`) |
247
248
  | `ndev doctor` | Run environment diagnostics and health checks |
248
249
  | `ndev logs` | View and tail service and virtual host logs |
249
250
  | `ndev clean` | Clean up downloads cache and stale runtime state |
@@ -69,6 +69,7 @@ ndev/linux/runtime/upgrade.py
69
69
  ndev/win/__init__.py
70
70
  ndev/win/cli.py
71
71
  ndev/win/tui.py
72
+ ndev/win/bin/shim.exe
72
73
  ndev/win/commands/__init__.py
73
74
  ndev/win/core/__init__.py
74
75
  ndev/win/core/db.py
@@ -85,6 +86,7 @@ ndev/win/core/pma.py
85
86
  ndev/win/core/redis_core.py
86
87
  ndev/win/core/services.py
87
88
  ndev/win/core/setup.py
89
+ ndev/win/core/shim.py
88
90
  ndev/win/core/upgrade.py
89
91
  ndev/win/core/vhost.py
90
92
  ndev/win/templates/vhost.conf.tmpl
@@ -94,4 +96,5 @@ ndev_stack.egg-info/SOURCES.txt
94
96
  ndev_stack.egg-info/dependency_links.txt
95
97
  ndev_stack.egg-info/entry_points.txt
96
98
  ndev_stack.egg-info/requires.txt
97
- ndev_stack.egg-info/top_level.txt
99
+ ndev_stack.egg-info/top_level.txt
100
+ tests/test_shim.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ndev-stack"
7
- version = "0.3.0"
7
+ version = "0.3.1"
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"
@@ -41,5 +41,5 @@ where = ["."]
41
41
  include = ["ndev*"]
42
42
 
43
43
  [tool.setuptools.package-data]
44
- "*" = ["*.tmpl", "*.conf", "*.json", "*.ini", "*.php", "*.md"]
44
+ "*" = ["*.tmpl", "*.conf", "*.json", "*.ini", "*.php", "*.md", "*.exe"]
45
45
 
@@ -0,0 +1,86 @@
1
+ """
2
+ Tests for ndev.win.core.shim (Windows shimgen integration).
3
+ """
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from ndev.win.core import shim
11
+
12
+
13
+ class TestShim(unittest.TestCase):
14
+ def setUp(self):
15
+ self.test_dir = Path(tempfile.mkdtemp(prefix="ndev_shim_test_"))
16
+
17
+ def tearDown(self):
18
+ shutil.rmtree(self.test_dir, ignore_errors=True)
19
+
20
+ def test_shim_exe_exists(self):
21
+ self.assertTrue(shim.SHIM_EXE.exists(), f"shim.exe not found at {shim.SHIM_EXE}")
22
+
23
+ def test_create_and_read_exe_shim(self):
24
+ # Target dummy executable
25
+ target_path = Path(r"C:\Windows\System32\cmd.exe")
26
+ shim_file = shim.create(
27
+ "dummy_cmd",
28
+ target_path,
29
+ args="/c echo hello",
30
+ cwd=r"C:\Windows",
31
+ path_prepend=r"C:\Windows\System32",
32
+ shim_dir=self.test_dir,
33
+ )
34
+
35
+ self.assertEqual(shim_file, self.test_dir / "dummy_cmd.exe")
36
+ self.assertTrue((self.test_dir / "dummy_cmd.exe").exists())
37
+ self.assertTrue((self.test_dir / "dummy_cmd.shim").exists())
38
+
39
+ cfg = shim.read_shim("dummy_cmd", shim_dir=self.test_dir)
40
+ self.assertEqual(cfg.get("path"), str(target_path))
41
+ self.assertEqual(cfg.get("args"), "/c echo hello")
42
+ self.assertEqual(cfg.get("cwd"), r"C:\Windows")
43
+ self.assertEqual(cfg.get("path_prepend"), r"C:\Windows\System32")
44
+
45
+ def test_list_and_remove_shim(self):
46
+ target_path = Path(r"C:\Windows\System32\cmd.exe")
47
+ shim.create("tool_a", target_path, shim_dir=self.test_dir)
48
+ shim.create("tool_b", target_path, shim_dir=self.test_dir)
49
+
50
+ shims = shim.list_shims(shim_dir=self.test_dir)
51
+ self.assertIn("tool_a", shims)
52
+ self.assertIn("tool_b", shims)
53
+
54
+ removed = shim.remove("tool_a", shim_dir=self.test_dir)
55
+ self.assertTrue(removed)
56
+ self.assertFalse((self.test_dir / "tool_a.exe").exists())
57
+ self.assertFalse((self.test_dir / "tool_a.shim").exists())
58
+
59
+ shims_after = shim.list_shims(shim_dir=self.test_dir)
60
+ self.assertNotIn("tool_a", shims_after)
61
+ self.assertIn("tool_b", shims_after)
62
+
63
+ def test_clean_legacy_option(self):
64
+ target_path = Path(r"C:\Windows\System32\cmd.exe")
65
+ # Create mock legacy .bat, .cmd, and .ps1
66
+ (self.test_dir / "preserve_test.bat").write_text("@echo off", encoding="utf-8")
67
+ (self.test_dir / "preserve_test.cmd").write_text("@echo off", encoding="utf-8")
68
+ (self.test_dir / "preserve_test.ps1").write_text("& echo hi", encoding="utf-8")
69
+
70
+ # When clean_legacy is False, legacy files remain
71
+ shim.create("preserve_test", target_path, shim_dir=self.test_dir, clean_legacy=False)
72
+ self.assertTrue((self.test_dir / "preserve_test.exe").exists())
73
+ self.assertTrue((self.test_dir / "preserve_test.bat").exists())
74
+ self.assertTrue((self.test_dir / "preserve_test.cmd").exists())
75
+ self.assertTrue((self.test_dir / "preserve_test.ps1").exists())
76
+
77
+ # When clean_legacy is True, legacy files are unlinked
78
+ shim.create("preserve_test", target_path, shim_dir=self.test_dir, clean_legacy=True)
79
+ self.assertTrue((self.test_dir / "preserve_test.exe").exists())
80
+ self.assertFalse((self.test_dir / "preserve_test.bat").exists())
81
+ self.assertFalse((self.test_dir / "preserve_test.cmd").exists())
82
+ self.assertFalse((self.test_dir / "preserve_test.ps1").exists())
83
+
84
+
85
+ if __name__ == "__main__":
86
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes