devctl 1.0.0__tar.gz → 1.1.0__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 (120) hide show
  1. {devctl-1.0.0 → devctl-1.1.0}/PKG-INFO +3 -2
  2. {devctl-1.0.0 → devctl-1.1.0}/README.md +2 -1
  3. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/angular.py +9 -2
  4. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/django.py +34 -6
  5. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/fastapi.py +14 -3
  6. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/go_fiber.py +9 -1
  7. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/nestjs.py +3 -0
  8. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/nextjs.py +3 -0
  9. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/nodejs.py +14 -2
  10. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/react.py +9 -1
  11. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_nestjs.py +3 -0
  12. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/svelte.py +9 -1
  13. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/vue.py +14 -2
  14. {devctl-1.0.0 → devctl-1.1.0}/devctl/orchestrator/runner.py +16 -12
  15. devctl-1.1.0/devctl/utils/__init__.py +5 -0
  16. {devctl-1.0.0 → devctl-1.1.0}/devctl/utils/dependencies.py +11 -1
  17. devctl-1.1.0/devctl/utils/platform.py +120 -0
  18. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/PKG-INFO +3 -2
  19. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/SOURCES.txt +2 -0
  20. {devctl-1.0.0 → devctl-1.1.0}/pyproject.toml +1 -1
  21. devctl-1.1.0/test/test_platform.py +72 -0
  22. devctl-1.0.0/devctl/utils/__init__.py +0 -3
  23. {devctl-1.0.0 → devctl-1.1.0}/LICENSE +0 -0
  24. {devctl-1.0.0 → devctl-1.1.0}/devctl/__init__.py +0 -0
  25. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/__init__.py +0 -0
  26. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/add.py +0 -0
  27. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/deploy.py +0 -0
  28. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/docker.py +0 -0
  29. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/init.py +0 -0
  30. {devctl-1.0.0 → devctl-1.1.0}/devctl/commands/run.py +0 -0
  31. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/__init__.py +0 -0
  32. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/docker_scaffold.py +0 -0
  33. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_angular.py +0 -0
  34. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_django.py +0 -0
  35. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_fastapi.py +0 -0
  36. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_go.py +0 -0
  37. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_nextjs.py +0 -0
  38. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_nodejs.py +0 -0
  39. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_react.py +0 -0
  40. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_spring.py +0 -0
  41. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_svelte.py +0 -0
  42. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/scaffold_vue.py +0 -0
  43. {devctl-1.0.0 → devctl-1.1.0}/devctl/generators/spring.py +0 -0
  44. {devctl-1.0.0 → devctl-1.1.0}/devctl/main.py +0 -0
  45. {devctl-1.0.0 → devctl-1.1.0}/devctl/orchestrator/__init__.py +0 -0
  46. {devctl-1.0.0 → devctl-1.1.0}/devctl/orchestrator/config_builder.py +0 -0
  47. {devctl-1.0.0 → devctl-1.1.0}/devctl/orchestrator/scanner.py +0 -0
  48. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/config/environment.development.ts.j2 +0 -0
  49. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/config/environment.ts.j2 +0 -0
  50. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/config/proxy.conf.json.j2 +0 -0
  51. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/models/request.model.ts.j2 +0 -0
  52. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/models/response.model.ts.j2 +0 -0
  53. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/form/form.component.html.j2 +0 -0
  54. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/form/form.component.scss.j2 +0 -0
  55. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/form/form.component.ts.j2 +0 -0
  56. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/list/list.component.html.j2 +0 -0
  57. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/list/list.component.scss.j2 +0 -0
  58. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/pages/list/list.component.ts.j2 +0 -0
  59. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/routes.ts.j2 +0 -0
  60. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/angular/feature/services/service.ts.j2 +0 -0
  61. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/deploy.yml.j2 +0 -0
  62. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/django/Dockerfile.j2 +0 -0
  63. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/fastapi/Dockerfile.j2 +0 -0
  64. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/frontend/Dockerfile.j2 +0 -0
  65. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/go/Dockerfile.j2 +0 -0
  66. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/nestjs/Dockerfile.j2 +0 -0
  67. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/nextjs/Dockerfile.j2 +0 -0
  68. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/nodejs/Dockerfile.j2 +0 -0
  69. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/spring/Dockerfile.j2 +0 -0
  70. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/docker/svelte/Dockerfile.j2 +0 -0
  71. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/proxy.conf.json.j2 +0 -0
  72. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/Controller.java.j2 +0 -0
  73. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/Entity.java.j2 +0 -0
  74. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/Repository.java.j2 +0 -0
  75. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/Service.java.j2 +0 -0
  76. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/ServiceImpl.java.j2 +0 -0
  77. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/application.properties.j2 +0 -0
  78. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/config/ApplicationConfig.java.j2 +0 -0
  79. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/config/JwtAuthenticationFilter.java.j2 +0 -0
  80. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/config/JwtService.java.j2 +0 -0
  81. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/config/SecurityConfig.java.j2 +0 -0
  82. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/docker-compose.yml.j2 +0 -0
  83. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/dto/Request.java.j2 +0 -0
  84. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/dto/Response.java.j2 +0 -0
  85. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/spring/mapper/Mapper.java.j2 +0 -0
  86. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/config/App.vue.j2 +0 -0
  87. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/config/main.ts.j2 +0 -0
  88. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/config/router.ts.j2 +0 -0
  89. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/config/vite.config.ts.j2 +0 -0
  90. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/feature/Form.vue.j2 +0 -0
  91. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/feature/List.vue.j2 +0 -0
  92. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/feature/models.ts.j2 +0 -0
  93. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/feature/routes.ts.j2 +0 -0
  94. {devctl-1.0.0 → devctl-1.1.0}/devctl/templates/vue/feature/service.ts.j2 +0 -0
  95. {devctl-1.0.0 → devctl-1.1.0}/devctl/utils/env_loader.py +0 -0
  96. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/dependency_links.txt +0 -0
  97. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/entry_points.txt +0 -0
  98. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/requires.txt +0 -0
  99. {devctl-1.0.0 → devctl-1.1.0}/devctl.egg-info/top_level.txt +0 -0
  100. {devctl-1.0.0 → devctl-1.1.0}/setup.cfg +0 -0
  101. {devctl-1.0.0 → devctl-1.1.0}/test/test_add.py +0 -0
  102. {devctl-1.0.0 → devctl-1.1.0}/test/test_commands.py +0 -0
  103. {devctl-1.0.0 → devctl-1.1.0}/test/test_deploy.py +0 -0
  104. {devctl-1.0.0 → devctl-1.1.0}/test/test_django.py +0 -0
  105. {devctl-1.0.0 → devctl-1.1.0}/test/test_docker_scaffold.py +0 -0
  106. {devctl-1.0.0 → devctl-1.1.0}/test/test_env_loader.py +0 -0
  107. {devctl-1.0.0 → devctl-1.1.0}/test/test_fastapi.py +0 -0
  108. {devctl-1.0.0 → devctl-1.1.0}/test/test_go.py +0 -0
  109. {devctl-1.0.0 → devctl-1.1.0}/test/test_init.py +0 -0
  110. {devctl-1.0.0 → devctl-1.1.0}/test/test_main.py +0 -0
  111. {devctl-1.0.0 → devctl-1.1.0}/test/test_mongodb.py +0 -0
  112. {devctl-1.0.0 → devctl-1.1.0}/test/test_nestjs.py +0 -0
  113. {devctl-1.0.0 → devctl-1.1.0}/test/test_nextjs.py +0 -0
  114. {devctl-1.0.0 → devctl-1.1.0}/test/test_nodejs.py +0 -0
  115. {devctl-1.0.0 → devctl-1.1.0}/test/test_pom_patcher.py +0 -0
  116. {devctl-1.0.0 → devctl-1.1.0}/test/test_react.py +0 -0
  117. {devctl-1.0.0 → devctl-1.1.0}/test/test_runner.py +0 -0
  118. {devctl-1.0.0 → devctl-1.1.0}/test/test_scanner.py +0 -0
  119. {devctl-1.0.0 → devctl-1.1.0}/test/test_svelte.py +0 -0
  120. {devctl-1.0.0 → devctl-1.1.0}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devctl
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Local development orchestrator for Spring Boot, Angular, and Vue.js
5
5
  Author: Youssef Fellah
6
6
  License: MIT License
@@ -45,10 +45,11 @@ Dynamic: license-file
45
45
  orchestrate the development lifecycle for modern full-stack architectures. It
46
46
  provides a unified workflow for managing backends (Spring Boot, NestJS, Go,
47
47
  FastAPI), frontends (Angular, Vue, React, NextJS, Svelte), and their
48
- containerization.
48
+ containerization across Linux, macOS, and Windows.
49
49
 
50
50
  ## Core Features
51
51
 
52
+ - **Cross-Platform Architecture**: Out-of-the-box support for Windows, Linux, and macOS utilizing a robust platform abstraction layer.
52
53
  - **Multi-Stack Orchestration**: Launch databases, multiple microservices, and
53
54
  frontend dev servers with a single command. Includes real-time, prefixed log
54
55
  streaming.
@@ -4,10 +4,11 @@
4
4
  orchestrate the development lifecycle for modern full-stack architectures. It
5
5
  provides a unified workflow for managing backends (Spring Boot, NestJS, Go,
6
6
  FastAPI), frontends (Angular, Vue, React, NextJS, Svelte), and their
7
- containerization.
7
+ containerization across Linux, macOS, and Windows.
8
8
 
9
9
  ## Core Features
10
10
 
11
+ - **Cross-Platform Architecture**: Out-of-the-box support for Windows, Linux, and macOS utilizing a robust platform abstraction layer.
11
12
  - **Multi-Stack Orchestration**: Launch databases, multiple microservices, and
12
13
  frontend dev servers with a single command. Includes real-time, prefixed log
13
14
  streaming.
@@ -79,8 +79,15 @@ def generate_angular_boilerplate(project_name: str) -> bool:
79
79
 
80
80
  safe_name = project_name.lower().replace("_", "-")
81
81
 
82
+ from devctl.utils import get_platform
83
+ platform = get_platform()
82
84
  try:
83
- subprocess.run(["ng", "version"], capture_output=True, check=True)
85
+ subprocess.run(
86
+ ["ng", "version"],
87
+ capture_output=True,
88
+ check=True,
89
+ shell=platform.shell_required,
90
+ )
84
91
  except FileNotFoundError:
85
92
  typer.secho(
86
93
  "Error: Angular CLI ('ng') not found on your system.",
@@ -95,7 +102,7 @@ def generate_angular_boilerplate(project_name: str) -> bool:
95
102
  command = ["ng", "new", safe_name, "--routing=true", "--style=scss", "--skip-git=true"]
96
103
 
97
104
  typer.secho("Downloading npm packages... (This may take 1-2 minutes)", fg=typer.colors.CYAN)
98
- subprocess.run(command, check=True)
105
+ subprocess.run(command, check=True, shell=platform.shell_required)
99
106
 
100
107
  # Post-installation configuration
101
108
  project_full_path = os.path.join(os.getcwd(), safe_name)
@@ -28,28 +28,56 @@ django-cors-headers
28
28
  with open(os.path.join(project_path, "requirements.txt"), "w") as f:
29
29
  f.write(requirements)
30
30
 
31
+ import sys
32
+ python_exe = "python" if sys.platform == "win32" else "python3"
33
+
31
34
  # 2. Create virtual environment
32
35
  typer.secho("Creating virtual environment...", fg=typer.colors.CYAN)
33
- subprocess.run(["python3", "-m", "venv", ".venv"], cwd=project_path, check=True)
36
+ subprocess.run(
37
+ [python_exe, "-m", "venv", ".venv"],
38
+ cwd=project_path,
39
+ check=True,
40
+ shell=(sys.platform == "win32"),
41
+ )
34
42
 
35
43
  # 3. Install Django
36
44
  typer.secho("Installing Django and DRF...", fg=typer.colors.CYAN)
37
- pip_path = os.path.join(".venv", "bin", "pip")
45
+ if sys.platform == "win32":
46
+ pip_path = os.path.join(".venv", "Scripts", "pip.exe")
47
+ else:
48
+ pip_path = os.path.join(".venv", "bin", "pip")
38
49
  subprocess.run(
39
50
  [pip_path, "install", "django", "djangorestframework"],
40
51
  cwd=project_path,
41
52
  check=True,
42
53
  stdout=subprocess.DEVNULL,
54
+ shell=(sys.platform == "win32"),
43
55
  )
44
56
 
45
57
  # 4. Start Project
46
58
  typer.secho("Scaffolding Django project structure...", fg=typer.colors.CYAN)
47
- django_admin = os.path.join(".venv", "bin", "django-admin")
48
- subprocess.run([django_admin, "startproject", safe_name, "."], cwd=project_path, check=True)
59
+ if sys.platform == "win32":
60
+ django_admin = os.path.join(".venv", "Scripts", "django-admin.exe")
61
+ else:
62
+ django_admin = os.path.join(".venv", "bin", "django-admin")
63
+ subprocess.run(
64
+ [django_admin, "startproject", safe_name, "."],
65
+ cwd=project_path,
66
+ check=True,
67
+ shell=(sys.platform == "win32"),
68
+ )
49
69
 
50
70
  # 5. Create core app
51
- python_path = os.path.join(".venv", "bin", "python")
52
- subprocess.run([python_path, "manage.py", "startapp", "core"], cwd=project_path, check=True)
71
+ if sys.platform == "win32":
72
+ python_path = os.path.join(".venv", "Scripts", "python.exe")
73
+ else:
74
+ python_path = os.path.join(".venv", "bin", "python")
75
+ subprocess.run(
76
+ [python_path, "manage.py", "startapp", "core"],
77
+ cwd=project_path,
78
+ check=True,
79
+ shell=(sys.platform == "win32"),
80
+ )
53
81
 
54
82
  typer.secho(
55
83
  f"Django project '{project_name}' successfully generated!", fg=typer.colors.GREEN
@@ -44,19 +44,30 @@ pydantic
44
44
  with open(os.path.join(project_path, "requirements.txt"), "w") as f:
45
45
  f.write(requirements)
46
46
 
47
+ import sys
48
+ python_exe = "python" if sys.platform == "win32" else "python3"
49
+
47
50
  # 3. Create virtual environment
48
51
  typer.secho("Creating virtual environment...", fg=typer.colors.CYAN)
49
- subprocess.run(["python3", "-m", "venv", ".venv"], cwd=project_path, check=True)
52
+ subprocess.run(
53
+ [python_exe, "-m", "venv", ".venv"],
54
+ cwd=project_path,
55
+ check=True,
56
+ shell=(sys.platform == "win32"),
57
+ )
50
58
 
51
59
  # 4. Install dependencies
52
60
  typer.secho("Installing dependencies (fastapi, uvicorn)...", fg=typer.colors.CYAN)
53
- # Note: on Linux it's .venv/bin/pip
54
- pip_path = os.path.join(".venv", "bin", "pip")
61
+ if sys.platform == "win32":
62
+ pip_path = os.path.join(".venv", "Scripts", "pip.exe")
63
+ else:
64
+ pip_path = os.path.join(".venv", "bin", "pip")
55
65
  subprocess.run(
56
66
  [pip_path, "install", "-r", "requirements.txt"],
57
67
  cwd=project_path,
58
68
  check=True,
59
69
  stdout=subprocess.DEVNULL,
70
+ shell=(sys.platform == "win32"),
60
71
  )
61
72
 
62
73
  typer.secho(f"FastAPI project '{safe_name}' successfully generated!", fg=typer.colors.GREEN)
@@ -19,9 +19,16 @@ def generate_go_boilerplate(project_name: str) -> bool:
19
19
  try:
20
20
  os.makedirs(project_path, exist_ok=True)
21
21
 
22
+ from devctl.utils import get_platform
23
+ platform = get_platform()
22
24
  # 1. Go mod init
23
25
  typer.secho("Initializing Go module...", fg=typer.colors.CYAN)
24
- subprocess.run(["go", "mod", "init", project_name], cwd=project_path, check=True)
26
+ subprocess.run(
27
+ ["go", "mod", "init", project_name],
28
+ cwd=project_path,
29
+ check=True,
30
+ shell=platform.shell_required,
31
+ )
25
32
 
26
33
  # 2. Install Fiber
27
34
  typer.secho("Installing Fiber framework...", fg=typer.colors.CYAN)
@@ -30,6 +37,7 @@ def generate_go_boilerplate(project_name: str) -> bool:
30
37
  cwd=project_path,
31
38
  check=True,
32
39
  stdout=subprocess.DEVNULL,
40
+ shell=platform.shell_required,
33
41
  )
34
42
 
35
43
  # 3. Create main.go
@@ -21,6 +21,8 @@ def generate_nest_boilerplate(project_name: str) -> bool:
21
21
  # --strict: enables strict mode
22
22
  # --skip-git: devctl might be in a git repo already
23
23
  typer.secho("Scaffolding NestJS project (this may take a minute)...", fg=typer.colors.CYAN)
24
+ from devctl.utils import get_platform
25
+ platform = get_platform()
24
26
  subprocess.run(
25
27
  [
26
28
  "npx",
@@ -34,6 +36,7 @@ def generate_nest_boilerplate(project_name: str) -> bool:
34
36
  "--skip-git",
35
37
  ],
36
38
  check=True,
39
+ shell=platform.shell_required,
37
40
  )
38
41
 
39
42
  typer.secho(
@@ -23,6 +23,8 @@ def generate_nextjs_boilerplate(project_name: str) -> bool:
23
23
  # --src-dir: Use src/ directory
24
24
  # --app: Use App Router
25
25
  # --import-alias: alias for imports
26
+ from devctl.utils import get_platform
27
+ platform = get_platform()
26
28
  subprocess.run(
27
29
  [
28
30
  "npx",
@@ -38,6 +40,7 @@ def generate_nextjs_boilerplate(project_name: str) -> bool:
38
40
  "--use-npm",
39
41
  ],
40
42
  check=True,
43
+ shell=platform.shell_required,
41
44
  )
42
45
 
43
46
  typer.secho(
@@ -21,10 +21,16 @@ def generate_nodejs_boilerplate(project_name: str) -> bool:
21
21
  try:
22
22
  os.makedirs(project_path, exist_ok=True)
23
23
 
24
+ from devctl.utils import get_platform
25
+ platform = get_platform()
24
26
  # 1. Initialize package.json
25
27
  typer.secho("Initializing package.json...", fg=typer.colors.CYAN)
26
28
  subprocess.run(
27
- ["npm", "init", "-y"], cwd=project_path, check=True, stdout=subprocess.DEVNULL
29
+ ["npm", "init", "-y"],
30
+ cwd=project_path,
31
+ check=True,
32
+ stdout=subprocess.DEVNULL,
33
+ shell=platform.shell_required,
28
34
  )
29
35
 
30
36
  # 2. Install dependencies
@@ -37,6 +43,7 @@ def generate_nodejs_boilerplate(project_name: str) -> bool:
37
43
  cwd=project_path,
38
44
  check=True,
39
45
  stdout=subprocess.DEVNULL,
46
+ shell=platform.shell_required,
40
47
  )
41
48
  subprocess.run(
42
49
  [
@@ -53,12 +60,17 @@ def generate_nodejs_boilerplate(project_name: str) -> bool:
53
60
  cwd=project_path,
54
61
  check=True,
55
62
  stdout=subprocess.DEVNULL,
63
+ shell=platform.shell_required,
56
64
  )
57
65
 
58
66
  # 3. Initialize TypeScript
59
67
  typer.secho("Configuring TypeScript...", fg=typer.colors.CYAN)
60
68
  subprocess.run(
61
- ["npx", "tsc", "--init"], cwd=project_path, check=True, stdout=subprocess.DEVNULL
69
+ ["npx", "tsc", "--init"],
70
+ cwd=project_path,
71
+ check=True,
72
+ stdout=subprocess.DEVNULL,
73
+ shell=platform.shell_required,
62
74
  )
63
75
 
64
76
  # 4. Create folder structure
@@ -17,16 +17,24 @@ def generate_react_boilerplate(project_name: str) -> bool:
17
17
  safe_name = project_name.lower().replace("_", "-")
18
18
 
19
19
  try:
20
+ from devctl.utils import get_platform
21
+ platform = get_platform()
20
22
  typer.secho("Scaffolding React project...", fg=typer.colors.CYAN)
21
23
  subprocess.run(
22
24
  ["npm", "create", "vite@latest", safe_name, "--", "--template", "react-ts"],
23
25
  check=True,
26
+ shell=platform.shell_required,
24
27
  )
25
28
 
26
29
  project_full_path = os.path.join(os.getcwd(), safe_name)
27
30
 
28
31
  typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN)
29
- subprocess.run(["npm", "install"], cwd=project_full_path, check=True)
32
+ subprocess.run(
33
+ ["npm", "install"],
34
+ cwd=project_full_path,
35
+ check=True,
36
+ shell=platform.shell_required,
37
+ )
30
38
 
31
39
  typer.secho(
32
40
  f"ReactJS frontend '{safe_name}' successfully generated!", fg=typer.colors.GREEN
@@ -25,6 +25,8 @@ def generate_nest_resource(resource_name: str, fields_str: str, root_path: str =
25
25
 
26
26
  typer.secho(f"⚙️ Generating NestJS resource '{resource_name}'...", fg=typer.colors.CYAN)
27
27
 
28
+ from devctl.utils import get_platform
29
+ platform = get_platform()
28
30
  try:
29
31
  # Use npx to run Nest CLI
30
32
  # 'g res' is shorthand for 'generate resource'
@@ -33,6 +35,7 @@ def generate_nest_resource(resource_name: str, fields_str: str, root_path: str =
33
35
  ["npx", "@nestjs/cli", "g", "resource", resource_lower, "--no-spec"],
34
36
  cwd=nest_root,
35
37
  check=True,
38
+ shell=platform.shell_required,
36
39
  )
37
40
 
38
41
  typer.secho(
@@ -18,6 +18,8 @@ def generate_svelte_boilerplate(project_name: str) -> bool:
18
18
 
19
19
  try:
20
20
  typer.secho("Scaffolding SvelteKit project...", fg=typer.colors.CYAN)
21
+ from devctl.utils import get_platform
22
+ platform = get_platform()
21
23
  # Using a non-interactive way to scaffold svelte
22
24
  # We'll use the 'skeleton' template with TypeScript
23
25
  subprocess.run(
@@ -34,12 +36,18 @@ def generate_svelte_boilerplate(project_name: str) -> bool:
34
36
  "--no-git",
35
37
  ],
36
38
  check=True,
39
+ shell=platform.shell_required,
37
40
  )
38
41
 
39
42
  project_full_path = os.path.join(os.getcwd(), safe_name)
40
43
 
41
44
  typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN)
42
- subprocess.run(["npm", "install"], cwd=project_full_path, check=True)
45
+ subprocess.run(
46
+ ["npm", "install"],
47
+ cwd=project_full_path,
48
+ check=True,
49
+ shell=platform.shell_required,
50
+ )
43
51
 
44
52
  typer.secho(f"Svelte project '{safe_name}' successfully generated!", fg=typer.colors.GREEN)
45
53
  return True
@@ -37,6 +37,8 @@ def setup_vue_router(project_path: str):
37
37
  """
38
38
  typer.secho("Installing and configuring vue-router...", fg=typer.colors.CYAN)
39
39
 
40
+ from devctl.utils import get_platform
41
+ platform = get_platform()
40
42
  try:
41
43
  # 1. NPM package installation
42
44
  subprocess.run(
@@ -44,6 +46,7 @@ def setup_vue_router(project_path: str):
44
46
  cwd=project_path,
45
47
  check=True,
46
48
  stdout=subprocess.DEVNULL,
49
+ shell=platform.shell_required,
47
50
  )
48
51
 
49
52
  # 2. Router directory creation
@@ -79,16 +82,25 @@ def generate_vue_boilerplate(project_name: str) -> bool:
79
82
  typer.secho(f"Generating Vue.js frontend '{project_name}' via Vite...", fg=typer.colors.CYAN)
80
83
  safe_name = project_name.lower().replace("_", "-")
81
84
 
85
+ from devctl.utils import get_platform
86
+ platform = get_platform()
82
87
  try:
83
88
  typer.secho("Scaffolding Vite project...", fg=typer.colors.CYAN)
84
89
  subprocess.run(
85
- ["npm", "create", "vite@latest", safe_name, "--", "--template", "vue-ts"], check=True
90
+ ["npm", "create", "vite@latest", safe_name, "--", "--template", "vue-ts"],
91
+ check=True,
92
+ shell=platform.shell_required,
86
93
  )
87
94
 
88
95
  project_full_path = os.path.join(os.getcwd(), safe_name)
89
96
 
90
97
  typer.secho("Installing npm dependencies...", fg=typer.colors.CYAN)
91
- subprocess.run(["npm", "install"], cwd=project_full_path, check=True)
98
+ subprocess.run(
99
+ ["npm", "install"],
100
+ cwd=project_full_path,
101
+ check=True,
102
+ shell=platform.shell_required,
103
+ )
92
104
 
93
105
  # --- CALL OUR TWO CONFIGURATORS ---
94
106
  setup_vue_proxy(project_full_path)
@@ -17,6 +17,7 @@ from rich.console import Console
17
17
  from rich.markup import escape
18
18
 
19
19
  from devctl.generators.docker_scaffold import DockerProject
20
+ from devctl.utils import get_platform
20
21
  from devctl.utils.env_loader import get_project_env
21
22
 
22
23
  console = Console()
@@ -56,6 +57,7 @@ def _launch_process(p: DockerProject, cmd: List[str], color: str, label: str):
56
57
  # Load environment variables including .env if present
57
58
  env = get_project_env(p.path)
58
59
 
60
+ platform = get_platform()
59
61
  proc = subprocess.Popen(
60
62
  cmd,
61
63
  cwd=str(p.path),
@@ -63,6 +65,7 @@ def _launch_process(p: DockerProject, cmd: List[str], color: str, label: str):
63
65
  stderr=subprocess.STDOUT,
64
66
  bufsize=1,
65
67
  env=env,
68
+ shell=platform.shell_required,
66
69
  )
67
70
  active_processes.append((p.name, proc))
68
71
 
@@ -82,7 +85,11 @@ def launch_dev_environment(projects: List[DockerProject], docker_composes: List[
82
85
  cleanup_and_exit(docker_composes)
83
86
 
84
87
  signal.signal(signal.SIGINT, signal_handler)
85
- signal.signal(signal.SIGTERM, signal_handler)
88
+ if sys.platform != "win32":
89
+ signal.signal(signal.SIGTERM, signal_handler)
90
+ else:
91
+ if hasattr(signal, "SIGBREAK"):
92
+ signal.signal(signal.SIGBREAK, signal_handler)
86
93
 
87
94
  try:
88
95
  # 1. Start Databases
@@ -105,10 +112,11 @@ def launch_dev_environment(projects: List[DockerProject], docker_composes: List[
105
112
  typer.echo("Waiting 5s for databases to initialize...")
106
113
  time.sleep(5)
107
114
 
115
+ platform = get_platform()
108
116
  # 2. Start Projects
109
117
  for p in projects:
110
118
  if p.kind == "spring":
111
- _launch_process(p, ["./mvnw", "spring-boot:run"], "green", "Spring Boot")
119
+ _launch_process(p, [platform.mvnw_cmd, "spring-boot:run"], "green", "Spring Boot")
112
120
 
113
121
  elif p.kind == "angular":
114
122
  _launch_process(p, ["npx", "ng", "serve"], "cyan", "Angular")
@@ -132,17 +140,17 @@ def launch_dev_environment(projects: List[DockerProject], docker_composes: List[
132
140
  _launch_process(p, ["npm", "run", "dev"], "green", "NodeJS")
133
141
 
134
142
  elif p.kind == "fastapi":
135
- venv_python = os.path.join(str(p.path), ".venv", "bin", "python3")
143
+ venv_python = platform.get_venv_python(p.path)
136
144
  if not os.path.exists(venv_python):
137
- venv_python = "python3"
145
+ venv_python = platform.python_exe
138
146
  _launch_process(
139
147
  p, [venv_python, "-m", "uvicorn", "main:app", "--reload"], "cyan", "FastAPI"
140
148
  )
141
149
 
142
150
  elif p.kind == "django":
143
- venv_python = os.path.join(str(p.path), ".venv", "bin", "python3")
151
+ venv_python = platform.get_venv_python(p.path)
144
152
  if not os.path.exists(venv_python):
145
- venv_python = "python3"
153
+ venv_python = platform.python_exe
146
154
  _launch_process(p, [venv_python, "manage.py", "runserver"], "green", "Django")
147
155
 
148
156
  elif p.kind == "go":
@@ -193,14 +201,10 @@ def launch_dev_environment(projects: List[DockerProject], docker_composes: List[
193
201
 
194
202
  def cleanup_and_exit(docker_composes: List[Path]):
195
203
  """Stops all active processes and docker containers."""
204
+ platform = get_platform()
196
205
  for name, proc in active_processes:
197
206
  typer.echo(f"Closing {name}...")
198
- proc.terminate()
199
- try:
200
- proc.wait(timeout=5)
201
- except subprocess.TimeoutExpired:
202
- typer.echo(f"Force killing {name}...")
203
- proc.kill()
207
+ platform.kill_process_tree(proc)
204
208
 
205
209
  for compose_path in docker_composes:
206
210
  typer.echo(f"Stopping Docker Compose DB in {compose_path}...")
@@ -0,0 +1,5 @@
1
+ """
2
+ General utility functions and helpers.
3
+ """
4
+
5
+ from devctl.utils.platform import get_platform as get_platform
@@ -13,7 +13,17 @@ def check_tool(tool_name: str, required_for: str = "this operation"):
13
13
  Check if a tool is available in the system PATH.
14
14
  If not, print a friendly error message and exit.
15
15
  """
16
- if shutil.which(tool_name) is None:
16
+ from devctl.utils import get_platform
17
+ platform = get_platform()
18
+
19
+ actual_tool = tool_name
20
+ if tool_name == "python3" and platform.python_exe == "python":
21
+ actual_tool = "python"
22
+
23
+ if shutil.which(actual_tool) is None:
24
+ if tool_name == "python3" and shutil.which("python") is not None:
25
+ return
26
+
17
27
  typer.secho(
18
28
  f"\nError: '{tool_name}' is not installed or not in your PATH.",
19
29
  fg=typer.colors.RED,
@@ -0,0 +1,120 @@
1
+ """
2
+ Platform abstraction layer to handle OS-specific command execution, paths, and process management.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from abc import ABC, abstractmethod
9
+ from pathlib import Path
10
+
11
+
12
+ class BasePlatform(ABC):
13
+ @property
14
+ @abstractmethod
15
+ def python_exe(self) -> str:
16
+ """Global python executable name."""
17
+ pass
18
+
19
+ @property
20
+ @abstractmethod
21
+ def mvnw_cmd(self) -> str:
22
+ """Maven wrapper command/script name."""
23
+ pass
24
+
25
+ @property
26
+ @abstractmethod
27
+ def shell_required(self) -> bool:
28
+ """Indicates if shell execution is required for subprocess commands."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def get_venv_python(self, project_path: Path) -> str:
33
+ """Returns the path to the python executable in the virtual environment."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def get_venv_pip(self, project_path: Path) -> str:
38
+ """Returns the path to the pip executable in the virtual environment."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ def get_venv_django_admin(self, project_path: Path) -> str:
43
+ """Returns the path to the django-admin executable in the virtual environment."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def kill_process_tree(self, proc: subprocess.Popen) -> None:
48
+ """Terminates the process and all its descendants."""
49
+ pass
50
+
51
+
52
+ class WindowsPlatform(BasePlatform):
53
+ @property
54
+ def python_exe(self) -> str:
55
+ return "python"
56
+
57
+ @property
58
+ def mvnw_cmd(self) -> str:
59
+ return "mvnw.cmd"
60
+
61
+ @property
62
+ def shell_required(self) -> bool:
63
+ return True
64
+
65
+ def get_venv_python(self, project_path: Path) -> str:
66
+ return os.path.join(str(project_path), ".venv", "Scripts", "python.exe")
67
+
68
+ def get_venv_pip(self, project_path: Path) -> str:
69
+ return os.path.join(str(project_path), ".venv", "Scripts", "pip.exe")
70
+
71
+ def get_venv_django_admin(self, project_path: Path) -> str:
72
+ return os.path.join(str(project_path), ".venv", "Scripts", "django-admin.exe")
73
+
74
+ def kill_process_tree(self, proc: subprocess.Popen) -> None:
75
+ try:
76
+ subprocess.run(
77
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
78
+ stdout=subprocess.DEVNULL,
79
+ stderr=subprocess.DEVNULL,
80
+ check=False,
81
+ )
82
+ except Exception:
83
+ pass
84
+
85
+
86
+ class UnixPlatform(BasePlatform):
87
+ @property
88
+ def python_exe(self) -> str:
89
+ return "python3"
90
+
91
+ @property
92
+ def mvnw_cmd(self) -> str:
93
+ return "./mvnw"
94
+
95
+ @property
96
+ def shell_required(self) -> bool:
97
+ return False
98
+
99
+ def get_venv_python(self, project_path: Path) -> str:
100
+ return os.path.join(str(project_path), ".venv", "bin", "python3")
101
+
102
+ def get_venv_pip(self, project_path: Path) -> str:
103
+ return os.path.join(str(project_path), ".venv", "bin", "pip")
104
+
105
+ def get_venv_django_admin(self, project_path: Path) -> str:
106
+ return os.path.join(str(project_path), ".venv", "bin", "django-admin")
107
+
108
+ def kill_process_tree(self, proc: subprocess.Popen) -> None:
109
+ proc.terminate()
110
+ try:
111
+ proc.wait(timeout=5)
112
+ except subprocess.TimeoutExpired:
113
+ proc.kill()
114
+
115
+
116
+ def get_platform() -> BasePlatform:
117
+ """Factory function to return the correct Platform implementation."""
118
+ if sys.platform == "win32":
119
+ return WindowsPlatform()
120
+ return UnixPlatform()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devctl
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Local development orchestrator for Spring Boot, Angular, and Vue.js
5
5
  Author: Youssef Fellah
6
6
  License: MIT License
@@ -45,10 +45,11 @@ Dynamic: license-file
45
45
  orchestrate the development lifecycle for modern full-stack architectures. It
46
46
  provides a unified workflow for managing backends (Spring Boot, NestJS, Go,
47
47
  FastAPI), frontends (Angular, Vue, React, NextJS, Svelte), and their
48
- containerization.
48
+ containerization across Linux, macOS, and Windows.
49
49
 
50
50
  ## Core Features
51
51
 
52
+ - **Cross-Platform Architecture**: Out-of-the-box support for Windows, Linux, and macOS utilizing a robust platform abstraction layer.
52
53
  - **Multi-Stack Orchestration**: Launch databases, multiple microservices, and
53
54
  frontend dev servers with a single command. Includes real-time, prefixed log
54
55
  streaming.
@@ -93,6 +93,7 @@ devctl/templates/vue/feature/service.ts.j2
93
93
  devctl/utils/__init__.py
94
94
  devctl/utils/dependencies.py
95
95
  devctl/utils/env_loader.py
96
+ devctl/utils/platform.py
96
97
  test/test_add.py
97
98
  test/test_commands.py
98
99
  test/test_deploy.py
@@ -107,6 +108,7 @@ test/test_mongodb.py
107
108
  test/test_nestjs.py
108
109
  test/test_nextjs.py
109
110
  test/test_nodejs.py
111
+ test/test_platform.py
110
112
  test/test_pom_patcher.py
111
113
  test/test_react.py
112
114
  test/test_runner.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devctl"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Local development orchestrator for Spring Boot, Angular, and Vue.js"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -0,0 +1,72 @@
1
+ import os
2
+ import subprocess
3
+ from pathlib import Path
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from devctl.utils.platform import (
7
+ UnixPlatform,
8
+ WindowsPlatform,
9
+ get_platform,
10
+ )
11
+
12
+
13
+ def test_get_platform():
14
+ """Ensure get_platform returns the correct implementation based on sys.platform."""
15
+ with patch("sys.platform", "win32"):
16
+ p = get_platform()
17
+ assert isinstance(p, WindowsPlatform)
18
+
19
+ with patch("sys.platform", "linux"):
20
+ p = get_platform()
21
+ assert isinstance(p, UnixPlatform)
22
+
23
+
24
+ def test_windows_platform():
25
+ """Ensure WindowsPlatform returns the correct values and calls taskkill."""
26
+ p = WindowsPlatform()
27
+ assert p.python_exe == "python"
28
+ assert p.mvnw_cmd == "mvnw.cmd"
29
+ assert p.shell_required is True
30
+
31
+ path = Path("proj")
32
+ assert p.get_venv_python(path) == os.path.join(
33
+ "proj", ".venv", "Scripts", "python.exe"
34
+ )
35
+ assert p.get_venv_pip(path) == os.path.join(
36
+ "proj", ".venv", "Scripts", "pip.exe"
37
+ )
38
+ assert p.get_venv_django_admin(path) == os.path.join(
39
+ "proj", ".venv", "Scripts", "django-admin.exe"
40
+ )
41
+
42
+ # Test kill_process_tree
43
+ mock_proc = MagicMock()
44
+ mock_proc.pid = 1234
45
+ with patch("subprocess.run") as mock_run:
46
+ p.kill_process_tree(mock_proc)
47
+ mock_run.assert_called_once_with(
48
+ ["taskkill", "/F", "/T", "/PID", "1234"],
49
+ stdout=subprocess.DEVNULL,
50
+ stderr=subprocess.DEVNULL,
51
+ check=False,
52
+ )
53
+
54
+
55
+ def test_unix_platform():
56
+ """Ensure UnixPlatform returns the correct values and terminates/kills process."""
57
+ p = UnixPlatform()
58
+ assert p.python_exe == "python3"
59
+ assert p.mvnw_cmd == "./mvnw"
60
+ assert p.shell_required is False
61
+
62
+ path = Path("proj")
63
+ assert p.get_venv_python(path) == os.path.join("proj", ".venv", "bin", "python3")
64
+ assert p.get_venv_pip(path) == os.path.join("proj", ".venv", "bin", "pip")
65
+ assert p.get_venv_django_admin(path) == os.path.join("proj", ".venv", "bin", "django-admin")
66
+
67
+ # Test kill_process_tree with normal termination
68
+ mock_proc = MagicMock()
69
+ p.kill_process_tree(mock_proc)
70
+ mock_proc.terminate.assert_called_once()
71
+ mock_proc.wait.assert_called_once_with(timeout=5)
72
+ mock_proc.kill.assert_not_called()
@@ -1,3 +0,0 @@
1
- """
2
- General utility functions and helpers.
3
- """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes